DataCo Global - uma empresa global do ramo de supply chain¶
A Empresa DataCo Global é uma companhia de escala global que atua no setor de supply chain envolvendo diversos produtos desde eletrônicos, brinquedos, acessórios esportivos, acessórios para acampamento e até livros.
Aqui será realizada uma análise detalhada das suas vendas e lucros diante de diversos aspectos como categorias, departamentos e localização de clientes, avaliando também problemas logísticos como atrasos em entregas e problemas com operações fraudulentas.
Venha descobrir insights valiosos sobre a empresa, incluindo como andam as suas vendas, os seus lucros e suas operações de logística!
# Importando a biblioteca para manipulação de bases de dados
import pandas as pd
import locale
#Importando a biblioteca para manipulação algébrica
import numpy as np
# Bibliotecas para a EDA
import missingno
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import matplotlib.pyplot as plt
import seaborn as sns
from wordcloud import WordCloud
# Estatística
from scipy.stats import skew
from scipy.stats import chi2_contingency
# importando as funções Stratified K-Fold e train_test_split
from sklearn.model_selection import train_test_split, StratifiedKFold
# Importando os modelos
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
from catboost import CatBoostClassifier
from imblearn.ensemble import BalancedRandomForestClassifier
# Feature Importance
from sklearn.inspection import permutation_importance
from sklearn.feature_selection import RFE
# importando as funções para calcular a precisão, revocação, precision_recall_auc, roc_auc, medida F1 e acurácia
from sklearn.metrics import precision_score, recall_score, precision_recall_curve, auc, roc_auc_score, f1_score, accuracy_score, confusion_matrix
# Encoder para tratamento de variáveis categóricas
from category_encoders import CatBoostEncoder
# Pipelines
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
# Importando biblioteca para tunagem de hiperparâmetros
import optuna as opt
# Setando diretório de trabalho
import os
os.chdir(r"C:\Users\edd-j\Downloads\Conteudos\Portfolio\Fraudes\Fraude_Supply_Chain")
# Configurar para não exibir warnings
from warnings import filterwarnings
filterwarnings('ignore')
# Exibindo todas as colunas das bases de dados
pd.set_option('display.max_columns', None)
# Ajustando a configuração de exibição do Pandas para mostrar todo o conteúdo das colunas
pd.set_option('display.max_colwidth', None)
# Importando bases de dados
df = pd.read_csv('DataCoSupplyChainDataset.csv', encoding='ISO-8859-1')
df_descricao = pd.read_csv('DescriptionDataCoSupplyChain.csv')
# Limpeza dos dados para remover caracteres extras como ':'
df_descricao['FIELDS'] = df_descricao['FIELDS'].str.strip()
df_descricao['DESCRIPTION'] = df_descricao['DESCRIPTION'].str.strip().str.lstrip(':').str.strip()
# Apresentando a descrição das colunas da base de dados
df_descricao
| FIELDS | DESCRIPTION | |
|---|---|---|
| 0 | Type | Type of transaction made |
| 1 | Days for shipping (real) | Actual shipping days of the purchased product |
| 2 | Days for shipment (scheduled) | Days of scheduled delivery of the purchased product |
| 3 | Benefit per order | Earnings per order placed |
| 4 | Sales per customer | Total sales per customer made per customer |
| 5 | Delivery Status | Delivery status of orders: Advance shipping , Late delivery , Shipping canceled , Shipping on time |
| 6 | Late_delivery_risk | Categorical variable that indicates if sending is late (1), it is not late (0). |
| 7 | Category Id | Product category code |
| 8 | Category Name | Description of the product category |
| 9 | Customer City | City where the customer made the purchase |
| 10 | Customer Country | Country where the customer made the purchase |
| 11 | Customer Email | Customer's email |
| 12 | Customer Fname | Customer name |
| 13 | Customer Id | Customer ID |
| 14 | Customer Lname | Customer lastname |
| 15 | Customer Password | Masked customer key |
| 16 | Customer Segment | Types of Customers: Consumer , Corporate , Home Office |
| 17 | Customer State | State to which the store where the purchase is registered belongs |
| 18 | Customer Street | Street to which the store where the purchase is registered belongs |
| 19 | Customer Zipcode | Customer Zipcode |
| 20 | Department Id | Department code of store |
| 21 | Department Name | Department name of store |
| 22 | Latitude | Latitude corresponding to location of store |
| 23 | Longitude | Longitude corresponding to location of store |
| 24 | Market | Market to where the order is delivered : Africa , Europe , LATAM , Pacific Asia , USCA |
| 25 | Order City | Destination city of the order |
| 26 | Order Country | Destination country of the order |
| 27 | Order Customer Id | Customer order code |
| 28 | order date (DateOrders) | Date on which the order is made |
| 29 | Order Id | Order code |
| 30 | Order Item Cardprod Id | Product code generated through the RFID reader |
| 31 | Order Item Discount | Order item discount value |
| 32 | Order Item Discount Rate | Order item discount percentage |
| 33 | Order Item Id | Order item code |
| 34 | Order Item Product Price | Price of products without discount |
| 35 | Order Item Profit Ratio | Order Item Profit Ratio |
| 36 | Order Item Quantity | Number of products per order |
| 37 | Sales | Value in sales |
| 38 | Order Item Total | Total amount per order |
| 39 | Order Profit Per Order | Order Profit Per Order |
| 40 | Order Region | Region of the world where the order is delivered : Southeast Asia ,South Asia ,Oceania ,Eastern Asia, West Asia , West of USA , US Center , West Africa, Central Africa ,North Africa ,Western Europe ,Northern , Caribbean , South America ,East Africa ,Southern Europe , East of USA ,Canada ,Southern Africa , Central Asia , Europe , Central America, Eastern Europe , South of USA |
| 41 | Order State | State of the region where the order is delivered |
| 42 | Order Status | Order Status : COMPLETE , PENDING , CLOSED , PENDING_PAYMENT ,CANCELED , PROCESSING ,SUSPECTED_FRAUD ,ON_HOLD ,PAYMENT_REVIEW |
| 43 | Product Card Id | Product code |
| 44 | Product Category Id | Product category code |
| 45 | Product Description | Product Description |
| 46 | Product Image | Link of visit and purchase of the product |
| 47 | Product Name | Product Name |
| 48 | Product Price | Product Price |
| 49 | Product Status | Status of the product stock :If it is 1 not available , 0 the product is available |
| 50 | Shipping date (DateOrders) | Exact date and time of shipment |
| 51 | Shipping Mode | The following shipping modes are presented : Standard Class , First Class , Second Class , Same Day |
# Primeiras linhas do dataset
df.head()
| Type | Days for shipping (real) | Days for shipment (scheduled) | Benefit per order | Sales per customer | Delivery Status | Late_delivery_risk | Category Id | Category Name | Customer City | Customer Country | Customer Email | Customer Fname | Customer Id | Customer Lname | Customer Password | Customer Segment | Customer State | Customer Street | Customer Zipcode | Department Id | Department Name | Latitude | Longitude | Market | Order City | Order Country | Order Customer Id | order date (DateOrders) | Order Id | Order Item Cardprod Id | Order Item Discount | Order Item Discount Rate | Order Item Id | Order Item Product Price | Order Item Profit Ratio | Order Item Quantity | Sales | Order Item Total | Order Profit Per Order | Order Region | Order State | Order Status | Order Zipcode | Product Card Id | Product Category Id | Product Description | Product Image | Product Name | Product Price | Product Status | shipping date (DateOrders) | Shipping Mode | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | DEBIT | 3 | 4 | 91.250000 | 314.640015 | Advance shipping | 0 | 73 | Sporting Goods | Caguas | Puerto Rico | XXXXXXXXX | Cally | 20755 | Holloway | XXXXXXXXX | Consumer | PR | 5365 Noble Nectar Island | 725.0 | 2 | Fitness | 18.251453 | -66.037056 | Pacific Asia | Bekasi | Indonesia | 20755 | 1/31/2018 22:56 | 77202 | 1360 | 13.110000 | 0.04 | 180517 | 327.75 | 0.29 | 1 | 327.75 | 314.640015 | 91.250000 | Southeast Asia | Java Occidental | COMPLETE | NaN | 1360 | 73 | NaN | http://images.acmesports.sports/Smart+watch | Smart watch | 327.75 | 0 | 2/3/2018 22:56 | Standard Class |
| 1 | TRANSFER | 5 | 4 | -249.089996 | 311.359985 | Late delivery | 1 | 73 | Sporting Goods | Caguas | Puerto Rico | XXXXXXXXX | Irene | 19492 | Luna | XXXXXXXXX | Consumer | PR | 2679 Rustic Loop | 725.0 | 2 | Fitness | 18.279451 | -66.037064 | Pacific Asia | Bikaner | India | 19492 | 1/13/2018 12:27 | 75939 | 1360 | 16.389999 | 0.05 | 179254 | 327.75 | -0.80 | 1 | 327.75 | 311.359985 | -249.089996 | South Asia | Rajastán | PENDING | NaN | 1360 | 73 | NaN | http://images.acmesports.sports/Smart+watch | Smart watch | 327.75 | 0 | 1/18/2018 12:27 | Standard Class |
| 2 | CASH | 4 | 4 | -247.779999 | 309.720001 | Shipping on time | 0 | 73 | Sporting Goods | San Jose | EE. UU. | XXXXXXXXX | Gillian | 19491 | Maldonado | XXXXXXXXX | Consumer | CA | 8510 Round Bear Gate | 95125.0 | 2 | Fitness | 37.292233 | -121.881279 | Pacific Asia | Bikaner | India | 19491 | 1/13/2018 12:06 | 75938 | 1360 | 18.030001 | 0.06 | 179253 | 327.75 | -0.80 | 1 | 327.75 | 309.720001 | -247.779999 | South Asia | Rajastán | CLOSED | NaN | 1360 | 73 | NaN | http://images.acmesports.sports/Smart+watch | Smart watch | 327.75 | 0 | 1/17/2018 12:06 | Standard Class |
| 3 | DEBIT | 3 | 4 | 22.860001 | 304.809998 | Advance shipping | 0 | 73 | Sporting Goods | Los Angeles | EE. UU. | XXXXXXXXX | Tana | 19490 | Tate | XXXXXXXXX | Home Office | CA | 3200 Amber Bend | 90027.0 | 2 | Fitness | 34.125946 | -118.291016 | Pacific Asia | Townsville | Australia | 19490 | 1/13/2018 11:45 | 75937 | 1360 | 22.940001 | 0.07 | 179252 | 327.75 | 0.08 | 1 | 327.75 | 304.809998 | 22.860001 | Oceania | Queensland | COMPLETE | NaN | 1360 | 73 | NaN | http://images.acmesports.sports/Smart+watch | Smart watch | 327.75 | 0 | 1/16/2018 11:45 | Standard Class |
| 4 | PAYMENT | 2 | 4 | 134.210007 | 298.250000 | Advance shipping | 0 | 73 | Sporting Goods | Caguas | Puerto Rico | XXXXXXXXX | Orli | 19489 | Hendricks | XXXXXXXXX | Corporate | PR | 8671 Iron Anchor Corners | 725.0 | 2 | Fitness | 18.253769 | -66.037048 | Pacific Asia | Townsville | Australia | 19489 | 1/13/2018 11:24 | 75936 | 1360 | 29.500000 | 0.09 | 179251 | 327.75 | 0.45 | 1 | 327.75 | 298.250000 | 134.210007 | Oceania | Queensland | PENDING_PAYMENT | NaN | 1360 | 73 | NaN | http://images.acmesports.sports/Smart+watch | Smart watch | 327.75 | 0 | 1/15/2018 11:24 | Standard Class |
print(f"O dataframe possui {df.shape[0]} linhas e {df.shape[1]} colunas.")
O dataframe possui 180519 linhas e 53 colunas.
# Informações sobre todas as colunas do dataset
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 180519 entries, 0 to 180518 Data columns (total 53 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Type 180519 non-null object 1 Days for shipping (real) 180519 non-null int64 2 Days for shipment (scheduled) 180519 non-null int64 3 Benefit per order 180519 non-null float64 4 Sales per customer 180519 non-null float64 5 Delivery Status 180519 non-null object 6 Late_delivery_risk 180519 non-null int64 7 Category Id 180519 non-null int64 8 Category Name 180519 non-null object 9 Customer City 180519 non-null object 10 Customer Country 180519 non-null object 11 Customer Email 180519 non-null object 12 Customer Fname 180519 non-null object 13 Customer Id 180519 non-null int64 14 Customer Lname 180511 non-null object 15 Customer Password 180519 non-null object 16 Customer Segment 180519 non-null object 17 Customer State 180519 non-null object 18 Customer Street 180519 non-null object 19 Customer Zipcode 180516 non-null float64 20 Department Id 180519 non-null int64 21 Department Name 180519 non-null object 22 Latitude 180519 non-null float64 23 Longitude 180519 non-null float64 24 Market 180519 non-null object 25 Order City 180519 non-null object 26 Order Country 180519 non-null object 27 Order Customer Id 180519 non-null int64 28 order date (DateOrders) 180519 non-null object 29 Order Id 180519 non-null int64 30 Order Item Cardprod Id 180519 non-null int64 31 Order Item Discount 180519 non-null float64 32 Order Item Discount Rate 180519 non-null float64 33 Order Item Id 180519 non-null int64 34 Order Item Product Price 180519 non-null float64 35 Order Item Profit Ratio 180519 non-null float64 36 Order Item Quantity 180519 non-null int64 37 Sales 180519 non-null float64 38 Order Item Total 180519 non-null float64 39 Order Profit Per Order 180519 non-null float64 40 Order Region 180519 non-null object 41 Order State 180519 non-null object 42 Order Status 180519 non-null object 43 Order Zipcode 24840 non-null float64 44 Product Card Id 180519 non-null int64 45 Product Category Id 180519 non-null int64 46 Product Description 0 non-null float64 47 Product Image 180519 non-null object 48 Product Name 180519 non-null object 49 Product Price 180519 non-null float64 50 Product Status 180519 non-null int64 51 shipping date (DateOrders) 180519 non-null object 52 Shipping Mode 180519 non-null object dtypes: float64(15), int64(14), object(24) memory usage: 73.0+ MB
numerics = ["int16", "int32", "int64", "float16", "float32", "float64"]
numericas = df.select_dtypes(include=numerics)
nao_numericas = df.select_dtypes(exclude=numerics)
print(f"Temos {numericas.shape[1]} colunas numéricas e {nao_numericas.shape[1]} colunas não-numéricas.")
Temos 29 colunas numéricas e 24 colunas não-numéricas.
# Estátísticas descritivas do dataset
df.describe()
| Days for shipping (real) | Days for shipment (scheduled) | Benefit per order | Sales per customer | Late_delivery_risk | Category Id | Customer Id | Customer Zipcode | Department Id | Latitude | Longitude | Order Customer Id | Order Id | Order Item Cardprod Id | Order Item Discount | Order Item Discount Rate | Order Item Id | Order Item Product Price | Order Item Profit Ratio | Order Item Quantity | Sales | Order Item Total | Order Profit Per Order | Order Zipcode | Product Card Id | Product Category Id | Product Description | Product Price | Product Status | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 180519.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 180516.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 180519.000000 | 24840.000000 | 180519.000000 | 180519.000000 | 0.0 | 180519.000000 | 180519.0 |
| mean | 3.497654 | 2.931847 | 21.974989 | 183.107609 | 0.548291 | 31.851451 | 6691.379495 | 35921.126914 | 5.443460 | 29.719955 | -84.915675 | 6691.379495 | 36221.894903 | 692.509764 | 20.664741 | 0.101668 | 90260.000000 | 141.232550 | 0.120647 | 2.127638 | 203.772096 | 183.107609 | 21.974989 | 55426.132327 | 692.509764 | 31.851451 | NaN | 141.232550 | 0.0 |
| std | 1.623722 | 1.374449 | 104.433526 | 120.043670 | 0.497664 | 15.640064 | 4162.918106 | 37542.461122 | 1.629246 | 9.813646 | 21.433241 | 4162.918106 | 21045.379569 | 336.446807 | 21.800901 | 0.070415 | 52111.490959 | 139.732492 | 0.466796 | 1.453451 | 132.273077 | 120.043670 | 104.433526 | 31919.279101 | 336.446807 | 15.640064 | NaN | 139.732492 | 0.0 |
| min | 0.000000 | 0.000000 | -4274.979980 | 7.490000 | 0.000000 | 2.000000 | 1.000000 | 603.000000 | 2.000000 | -33.937553 | -158.025986 | 1.000000 | 1.000000 | 19.000000 | 0.000000 | 0.000000 | 1.000000 | 9.990000 | -2.750000 | 1.000000 | 9.990000 | 7.490000 | -4274.979980 | 1040.000000 | 19.000000 | 2.000000 | NaN | 9.990000 | 0.0 |
| 25% | 2.000000 | 2.000000 | 7.000000 | 104.379997 | 0.000000 | 18.000000 | 3258.500000 | 725.000000 | 4.000000 | 18.265432 | -98.446312 | 3258.500000 | 18057.000000 | 403.000000 | 5.400000 | 0.040000 | 45130.500000 | 50.000000 | 0.080000 | 1.000000 | 119.980003 | 104.379997 | 7.000000 | 23464.000000 | 403.000000 | 18.000000 | NaN | 50.000000 | 0.0 |
| 50% | 3.000000 | 4.000000 | 31.520000 | 163.990005 | 1.000000 | 29.000000 | 6457.000000 | 19380.000000 | 5.000000 | 33.144863 | -76.847908 | 6457.000000 | 36140.000000 | 627.000000 | 14.000000 | 0.100000 | 90260.000000 | 59.990002 | 0.270000 | 1.000000 | 199.919998 | 163.990005 | 31.520000 | 59405.000000 | 627.000000 | 29.000000 | NaN | 59.990002 | 0.0 |
| 75% | 5.000000 | 4.000000 | 64.800003 | 247.399994 | 1.000000 | 45.000000 | 9779.000000 | 78207.000000 | 7.000000 | 39.279617 | -66.370583 | 9779.000000 | 54144.000000 | 1004.000000 | 29.990000 | 0.160000 | 135389.500000 | 199.990005 | 0.360000 | 3.000000 | 299.950012 | 247.399994 | 64.800003 | 90008.000000 | 1004.000000 | 45.000000 | NaN | 199.990005 | 0.0 |
| max | 6.000000 | 4.000000 | 911.799988 | 1939.989990 | 1.000000 | 76.000000 | 20757.000000 | 99205.000000 | 12.000000 | 48.781933 | 115.263077 | 20757.000000 | 77204.000000 | 1363.000000 | 500.000000 | 0.250000 | 180519.000000 | 1999.989990 | 0.500000 | 5.000000 | 1999.989990 | 1939.989990 | 911.799988 | 99301.000000 | 1363.000000 | 76.000000 | NaN | 1999.989990 | 0.0 |
3.1 Detecção de Outliers¶
Esta etapa consiste em detectar a quantidade de outliers presente em cada uma das variáveis da base de dados. São calculados utilizando Q1 (primeiro quartil), Q3 (terceiro quartil) e IQR (Distância Interquartil):
Q1: Este é o valor que separa os 25% menores valores da coluna. Também é conhecido como primeiro quartil.
Q3: Este é o valor que separa os 25% maiores valores da coluna. Também é conhecido como terceiro quartil.
$$IQR = Q3 - Q1$$
Será considerado Outlier quando o valor da variável (VAR):
$$VAR > Q3 + 1.5IQR$$ $$VAR < Q1 - 1.5IQR$$
# Função para identificar outliers em uma variável
def detect_outliers(column):
Q1 = column.quantile(0.25)
Q3 = column.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = column[(column < lower_bound) | (column > upper_bound)]
return outliers
# Dicionário para armazenar os outliers de cada variável
outliers_dict = {}
# Loop através das variáveis numéricas
for col in df.select_dtypes(include=['float64', 'int64']):
outliers = detect_outliers(df[col])
outliers_dict[col] = outliers
# Crie um DataFrame com os outliers
outliers_df = pd.DataFrame(outliers_dict)
# Conte quantos outliers cada variável possui
contagem_outliers = outliers_df.count()
# Exiba a contagem de outliers para cada variável
contagem_outliers_df = pd.DataFrame({'Variavel': contagem_outliers.index, 'Quantidade de Outliers': contagem_outliers.values})
contagem_outliers_df
| Variavel | Quantidade de Outliers | |
|---|---|---|
| 0 | Days for shipping (real) | 0 |
| 1 | Days for shipment (scheduled) | 0 |
| 2 | Benefit per order | 18942 |
| 3 | Sales per customer | 1943 |
| 4 | Late_delivery_risk | 0 |
| 5 | Category Id | 0 |
| 6 | Customer Id | 1198 |
| 7 | Customer Zipcode | 0 |
| 8 | Department Id | 362 |
| 9 | Latitude | 9 |
| 10 | Longitude | 1414 |
| 11 | Order Customer Id | 1198 |
| 12 | Order Id | 0 |
| 13 | Order Item Cardprod Id | 0 |
| 14 | Order Item Discount | 7537 |
| 15 | Order Item Discount Rate | 0 |
| 16 | Order Item Id | 0 |
| 17 | Order Item Product Price | 2048 |
| 18 | Order Item Profit Ratio | 17300 |
| 19 | Order Item Quantity | 0 |
| 20 | Sales | 488 |
| 21 | Order Item Total | 1943 |
| 22 | Order Profit Per Order | 18942 |
| 23 | Order Zipcode | 0 |
| 24 | Product Card Id | 0 |
| 25 | Product Category Id | 0 |
| 26 | Product Description | 0 |
| 27 | Product Price | 2048 |
| 28 | Product Status | 0 |
3.2 Valores Nulos¶
# Fazendo a soma de valores nulos por coluna
df.isnull().sum()
Type 0 Days for shipping (real) 0 Days for shipment (scheduled) 0 Benefit per order 0 Sales per customer 0 Delivery Status 0 Late_delivery_risk 0 Category Id 0 Category Name 0 Customer City 0 Customer Country 0 Customer Email 0 Customer Fname 0 Customer Id 0 Customer Lname 8 Customer Password 0 Customer Segment 0 Customer State 0 Customer Street 0 Customer Zipcode 3 Department Id 0 Department Name 0 Latitude 0 Longitude 0 Market 0 Order City 0 Order Country 0 Order Customer Id 0 order date (DateOrders) 0 Order Id 0 Order Item Cardprod Id 0 Order Item Discount 0 Order Item Discount Rate 0 Order Item Id 0 Order Item Product Price 0 Order Item Profit Ratio 0 Order Item Quantity 0 Sales 0 Order Item Total 0 Order Profit Per Order 0 Order Region 0 Order State 0 Order Status 0 Order Zipcode 155679 Product Card Id 0 Product Category Id 0 Product Description 180519 Product Image 0 Product Name 0 Product Price 0 Product Status 0 shipping date (DateOrders) 0 Shipping Mode 0 dtype: int64
# visualizando as colunas numéricas com dados faltantes
missingno.matrix(numericas,figsize=(40,10))
<Axes: >
Pela imagem acima, verifica-se que há duas colunas numéricas com grande quantidade de valores nulos: Order Zipcode e Product Description. pela contagem anterior, sabe-se que uma terceira coluna (Customer Zipcode) possui apenas 3 valores nulos, o que não foi possível verificar visualmente devido à baixa quantidade.
# visualizando as colunas não numéricas com dados faltantes
missingno.matrix(nao_numericas,figsize=(40,10))
<Axes: >
Pela imagem acima, não é possível identificar visualmente alguma variável não numérica com valores nulos. Porém, sabe se a variável Customer Lname possui 8 valores nulos em sua composição, não sendo possível verificar visualmente por ser uma quantidade pequena perante o número de registros da base.
# fazendo unpack de linhas e colunas
rows, columns = df.shape
# Percentual de dados faltantes por coluna
percentual_nan = ((df.isnull().sum()/rows) * 100).round(2)
percentual_nan
Type 0.00 Days for shipping (real) 0.00 Days for shipment (scheduled) 0.00 Benefit per order 0.00 Sales per customer 0.00 Delivery Status 0.00 Late_delivery_risk 0.00 Category Id 0.00 Category Name 0.00 Customer City 0.00 Customer Country 0.00 Customer Email 0.00 Customer Fname 0.00 Customer Id 0.00 Customer Lname 0.00 Customer Password 0.00 Customer Segment 0.00 Customer State 0.00 Customer Street 0.00 Customer Zipcode 0.00 Department Id 0.00 Department Name 0.00 Latitude 0.00 Longitude 0.00 Market 0.00 Order City 0.00 Order Country 0.00 Order Customer Id 0.00 order date (DateOrders) 0.00 Order Id 0.00 Order Item Cardprod Id 0.00 Order Item Discount 0.00 Order Item Discount Rate 0.00 Order Item Id 0.00 Order Item Product Price 0.00 Order Item Profit Ratio 0.00 Order Item Quantity 0.00 Sales 0.00 Order Item Total 0.00 Order Profit Per Order 0.00 Order Region 0.00 Order State 0.00 Order Status 0.00 Order Zipcode 86.24 Product Card Id 0.00 Product Category Id 0.00 Product Description 100.00 Product Image 0.00 Product Name 0.00 Product Price 0.00 Product Status 0.00 shipping date (DateOrders) 0.00 Shipping Mode 0.00 dtype: float64
3.3 Linhas Duplicadas¶
# verificando se há linhas duplicadas
df[df.duplicated()]
| Type | Days for shipping (real) | Days for shipment (scheduled) | Benefit per order | Sales per customer | Delivery Status | Late_delivery_risk | Category Id | Category Name | Customer City | Customer Country | Customer Email | Customer Fname | Customer Id | Customer Lname | Customer Password | Customer Segment | Customer State | Customer Street | Customer Zipcode | Department Id | Department Name | Latitude | Longitude | Market | Order City | Order Country | Order Customer Id | order date (DateOrders) | Order Id | Order Item Cardprod Id | Order Item Discount | Order Item Discount Rate | Order Item Id | Order Item Product Price | Order Item Profit Ratio | Order Item Quantity | Sales | Order Item Total | Order Profit Per Order | Order Region | Order State | Order Status | Order Zipcode | Product Card Id | Product Category Id | Product Description | Product Image | Product Name | Product Price | Product Status | shipping date (DateOrders) | Shipping Mode |
|---|
Nota-se que o dataset contem 180.519 entradas com informações de vendas de produtos da empresa SupplyAll e que todos os registros são únicos. Desta forma, é possível concluir que não existem entradas duplicadas.
3.4 Valores Diferentes por Coluna¶
# Definindo quantos valores diferentes existem em cada variável
contagem = pd.DataFrame(columns=['Variavel','Contagens_Distintas'])
for coluna in df.columns:
dados = pd.DataFrame({'Variavel': [coluna], 'Contagens_Distintas': [df[coluna].value_counts().shape[0]]})
contagem = pd.concat([contagem, dados], ignore_index = True)
contagem
| Variavel | Contagens_Distintas | |
|---|---|---|
| 0 | Type | 4 |
| 1 | Days for shipping (real) | 7 |
| 2 | Days for shipment (scheduled) | 4 |
| 3 | Benefit per order | 21998 |
| 4 | Sales per customer | 2927 |
| 5 | Delivery Status | 4 |
| 6 | Late_delivery_risk | 2 |
| 7 | Category Id | 51 |
| 8 | Category Name | 50 |
| 9 | Customer City | 563 |
| 10 | Customer Country | 2 |
| 11 | Customer Email | 1 |
| 12 | Customer Fname | 782 |
| 13 | Customer Id | 20652 |
| 14 | Customer Lname | 1109 |
| 15 | Customer Password | 1 |
| 16 | Customer Segment | 3 |
| 17 | Customer State | 46 |
| 18 | Customer Street | 7458 |
| 19 | Customer Zipcode | 995 |
| 20 | Department Id | 11 |
| 21 | Department Name | 11 |
| 22 | Latitude | 11250 |
| 23 | Longitude | 4487 |
| 24 | Market | 5 |
| 25 | Order City | 3597 |
| 26 | Order Country | 164 |
| 27 | Order Customer Id | 20652 |
| 28 | order date (DateOrders) | 65752 |
| 29 | Order Id | 65752 |
| 30 | Order Item Cardprod Id | 118 |
| 31 | Order Item Discount | 1017 |
| 32 | Order Item Discount Rate | 18 |
| 33 | Order Item Id | 180519 |
| 34 | Order Item Product Price | 75 |
| 35 | Order Item Profit Ratio | 162 |
| 36 | Order Item Quantity | 5 |
| 37 | Sales | 193 |
| 38 | Order Item Total | 2927 |
| 39 | Order Profit Per Order | 21998 |
| 40 | Order Region | 23 |
| 41 | Order State | 1089 |
| 42 | Order Status | 9 |
| 43 | Order Zipcode | 609 |
| 44 | Product Card Id | 118 |
| 45 | Product Category Id | 51 |
| 46 | Product Description | 0 |
| 47 | Product Image | 118 |
| 48 | Product Name | 118 |
| 49 | Product Price | 75 |
| 50 | Product Status | 1 |
| 51 | shipping date (DateOrders) | 63701 |
| 52 | Shipping Mode | 4 |
Com a informação das contagens distintas de cada variável, podemos obter as seguintes informações:
- Os clientes pagaram os produtos através de 4 meios de pagamentos diferentes
- Há 9 status diferentes para os pedidos
- Há 4 status diferentes para as entregas dos produtos
- Os produtos são entregues para 5 grandes regiões diferentes do mundo
- Os produtos foram enviados para 164 países diferentes
- Os clientes são de 2 países diferentes
- Há 51 categorias de produtos diferentes
- Há 3 segmentos distintos de clientes
- Há 20.652 clientes no total
- Há 23 micro regiões do mundo onde os produtos são entregues
# Desconsiderar pedidos que tiveram entrega cancelada. Está sendo levado em consideração que entregas canceladas têm o dinheiro
# da compra enviado de volta ao cliente, não contribuindo para os cofres da companhia
df_filtrado = df[df['Delivery Status']!='Shipping canceled']
# Transformando a variável 'order date (DateOrders)' em datetime para trabalhar com datas
df['order date (DateOrders)'] = pd.to_datetime(df['order date (DateOrders)'])
# Extraindo o ano
df['Ano'] = df['order date (DateOrders)'].dt.year
# Agrupando por Customer_Id e Year, e contando os Order_Id únicos
compras_por_cliente_ano = df.groupby(['Customer Id', 'Ano'])['Order Id'].nunique().reset_index()
# Renomeando a coluna para refletir a contagem
compras_por_cliente_ano.rename(columns={'Order Id': 'Quantidade de Pedidos'}, inplace=True)
compras_por_cliente_ano_pivot = compras_por_cliente_ano.pivot_table(index="Customer Id",columns="Ano",values="Quantidade de Pedidos",fill_value=0)
compras_por_cliente_ano_pivot
| Ano | 2015 | 2016 | 2017 | 2018 |
|---|---|---|---|---|
| Customer Id | ||||
| 1 | 1.0 | 0.0 | 0.0 | 0.0 |
| 2 | 1.0 | 1.0 | 2.0 | 0.0 |
| 3 | 1.0 | 1.0 | 3.0 | 0.0 |
| 4 | 2.0 | 1.0 | 1.0 | 0.0 |
| 5 | 0.0 | 3.0 | 0.0 | 0.0 |
| ... | ... | ... | ... | ... |
| 20753 | 0.0 | 0.0 | 0.0 | 1.0 |
| 20754 | 0.0 | 0.0 | 0.0 | 1.0 |
| 20755 | 0.0 | 0.0 | 0.0 | 1.0 |
| 20756 | 0.0 | 0.0 | 0.0 | 1.0 |
| 20757 | 0.0 | 0.0 | 0.0 | 1.0 |
20652 rows × 4 columns
# Filtrando todos os clientes que compraram em 2018
compras_por_cliente_ano_pivot[compras_por_cliente_ano_pivot[2018] > 0].value_counts()
2015 2016 2017 2018 0.0 0.0 0.0 1.0 2123 Name: count, dtype: int64
Chega-se a conclusão que, todos os clientes que compraram em 2018, compraram apenas em 2018!
# Filtrando para trazer somente a informação do ano de 2018
compras_2018 = compras_por_cliente_ano[compras_por_cliente_ano['Ano'] == 2018]
# Verificando se há clientes que compraram mais de uma vez em 2018
clientes_compras_multiplos_2018 = compras_2018[compras_2018['Quantidade de Pedidos'] > 1]
clientes_compras_multiplos_2018
| Customer Id | Ano | Quantidade de Pedidos |
|---|
Observa-se que, todos os clientes que compraram em 2018, compraram apenas em 2018 e apenas uma vez! Para verificar se este comportamento atípico começa antes de 2018, será analisado também o ano de 2017.
# Filtrando para o ano de 2017
compras_2017 = compras_por_cliente_ano[compras_por_cliente_ano['Ano'] == 2017]
# Certificando-se de que os IDs estão em ordem crescente
compras_2017 = compras_2017.sort_values(by='Customer Id')
# Encontrar a partir de qual Customer_Id todos os seguintes compraram apenas uma vez
starting_customer_id = None
for i in range(len(compras_2017)):
current_id = compras_2017.iloc[i]['Customer Id']
current_purchases = compras_2017.iloc[i]['Quantidade de Pedidos']
if current_purchases != 1:
continue # Pula se o cliente atual fez mais de uma compra
# Verifica se todos os IDs seguintes estão em sequência e têm apenas uma compra
if all((compras_2017.iloc[j]['Customer Id'] == current_id + j - i) and
(compras_2017.iloc[j]['Quantidade de Pedidos'] == 1) for j in range(i, len(compras_2017))):
starting_customer_id = current_id
break
if starting_customer_id is not None:
print(f"Todos os Customer_Id a partir de {starting_customer_id} fizeram apenas uma compra em 2017.")
else:
print("Não há uma sequência contínua de clientes com apenas uma compra.")
Todos os Customer_Id a partir de 12440 fizeram apenas uma compra em 2017.
Sendo assim, entre todos os Custumer_Id a partir de 12440, foi verificado a partir de que data eles começaram a comprar.
df['order date (DateOrders)'] = pd.to_datetime(df['order date (DateOrders)'])
# Filtrando 'Customer Id' a partir de 12440
df_customer_filtrado = df[df['Customer Id'] >= 12440]
# Obtendo a data mínima
data_minima = df_customer_filtrado['order date (DateOrders)'].min()
print("Data mínima para Customer Id a partir de 12440:", data_minima)
Data mínima para Customer Id a partir de 12440: 2017-10-02 13:50:00
df[df['Customer Id'] == 12440]
| Type | Days for shipping (real) | Days for shipment (scheduled) | Benefit per order | Sales per customer | Delivery Status | Late_delivery_risk | Category Id | Category Name | Customer City | Customer Country | Customer Email | Customer Fname | Customer Id | Customer Lname | Customer Password | Customer Segment | Customer State | Customer Street | Customer Zipcode | Department Id | Department Name | Latitude | Longitude | Market | Order City | Order Country | Order Customer Id | order date (DateOrders) | Order Id | Order Item Cardprod Id | Order Item Discount | Order Item Discount Rate | Order Item Id | Order Item Product Price | Order Item Profit Ratio | Order Item Quantity | Sales | Order Item Total | Order Profit Per Order | Order Region | Order State | Order Status | Order Zipcode | Product Card Id | Product Category Id | Product Description | Product Image | Product Name | Product Price | Product Status | shipping date (DateOrders) | Shipping Mode | Ano | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 24764 | DEBIT | 2 | 1 | 1.98 | 26.42 | Late delivery | 1 | 59 | Books | San Marcos | EE. UU. | XXXXXXXXX | Joanha | 12440 | Mirkckociv | XXXXXXXXX | Corporate | CA | 8324 Little Common | 92069.0 | 8 | Book Shop | 33.146751 | -117.169533 | Europe | Forst | Alemania | 12440 | 2017-10-02 13:50:00 | 68887 | 1346 | 4.66 | 0.15 | 172202 | 31.08 | 0.08 | 1 | 31.08 | 26.42 | 1.98 | Western Europe | Brandenburgo | COMPLETE | NaN | 1346 | 59 | NaN | http://images.acmesports.sports/Sports+Books | Sports Books | 31.08 | 0 | 10/4/2017 13:50 | First Class | 2017 |
df[(df['Customer Id'] < 12440) & (df['order date (DateOrders)'] >= data_minima)]
| Type | Days for shipping (real) | Days for shipment (scheduled) | Benefit per order | Sales per customer | Delivery Status | Late_delivery_risk | Category Id | Category Name | Customer City | Customer Country | Customer Email | Customer Fname | Customer Id | Customer Lname | Customer Password | Customer Segment | Customer State | Customer Street | Customer Zipcode | Department Id | Department Name | Latitude | Longitude | Market | Order City | Order Country | Order Customer Id | order date (DateOrders) | Order Id | Order Item Cardprod Id | Order Item Discount | Order Item Discount Rate | Order Item Id | Order Item Product Price | Order Item Profit Ratio | Order Item Quantity | Sales | Order Item Total | Order Profit Per Order | Order Region | Order State | Order Status | Order Zipcode | Product Card Id | Product Category Id | Product Description | Product Image | Product Name | Product Price | Product Status | shipping date (DateOrders) | Shipping Mode | Ano |
|---|
Conclusão: A partir de 2017-10-02, todos os clientes são novos e compraram apenas uma vez. Sendo assim, para não trabalhar com uma base de dados com uma possível interferência (erro operacional), o conjunto de dados considerará dos dados até setembro/2017.
# Atualizando os dataframes df_filtrado e df com o intervalo de datas correto, para seguir com as análises
df = pd.read_csv('DataCoSupplyChainDataset.csv', encoding='ISO-8859-1')
# Transformando a coluna de data para o formato data e hora
df['order date (DateOrders)'] = pd.to_datetime(df['order date (DateOrders)'])
# Definindo a data limite para ser considerada na análise
data_limite = pd.Timestamp('2017-09-30 23:59:59')
# Filtrando o dataframe df para conter datas menores ou iguais a que 2017-09-30
df = df[df['order date (DateOrders)'] <= data_limite]
# Desconsiderar pedidos que tiveram entrega cancelada
df_filtrado = df[df['Delivery Status']!='Shipping canceled']
numericas = df_filtrado.select_dtypes(include=numerics)
# Remover colunas específicas
numericas_filtradas = numericas.drop(['Category Id', 'Customer Id', 'Customer Zipcode', 'Department Id', 'Latitude',
'Longitude', 'Order Customer Id', 'Order Id', 'Order Item Cardprod Id', 'Order Item Id',
'Order Zipcode', 'Product Card Id', 'Product Category Id', 'Product Description',
'Product Status'], axis=1)
# Calcular a matriz de correlação
corr_matrix = numericas_filtradas.corr()
# Criar o heatmap
plt.figure(figsize=(16, 9))
sns.heatmap(corr_matrix, annot=True, fmt=".2f", cmap='RdBu')
plt.title('Correlação entre variáveis', fontsize=16)
plt.show()
Variáveis altamente correlacionadas¶
- Benefit per order e Order Profit Per Order (correlação 1.00)
- Sales per customer e Sales (correlação 0.99)
- Sales per Customer e Order Item Total (correlação 1.00)
- Order Item Product Price e Product Price (correlação 1.00)
Obs: É importante lembrar que a correlação não implica causalidade. Mesmo quando duas variáveis estão correlacionadas, isso não significa necessariamente que uma causa a outra. Outros fatores ou variáveis não incluídas na análise podem estar contribuindo para a relação observada. Portanto, a interpretação da correlação deve ser feita com cuidado e, quando necessário, devem ser realizadas análises adicionais para entender melhor a relação entre as variáveis.
# Calcula a quantidade de vendas por mes
vendas_por_mes = df_filtrado.resample('M', on='order date (DateOrders)').size()
# Criando um dataframe a partir da série
vendas_por_mes = vendas_por_mes.reset_index()
vendas_por_mes.rename(columns={0: 'Vendas (em quantidade)'}, inplace=True)
# Calculando a variação das vendas
vendas_por_mes['Variação de Vendas'] = vendas_por_mes['Vendas (em quantidade)'].diff()
# Calculando a variação percentual das vendas
vendas_por_mes['Variação Percentual'] = vendas_por_mes['Vendas (em quantidade)'].pct_change()
# Convertendo a variação para porcentagem e arredondando
vendas_por_mes['Variação Percentual'] = (vendas_por_mes['Variação Percentual'] * 100).round(2)
# Visualização
fig = px.line(vendas_por_mes, x='order date (DateOrders)', y='Vendas (em quantidade)',
title='Vendas totais por mês (em quantidade)', labels={'order date (DateOrders)': 'Data'})
fig.update_layout(width=1000, height=600)
fig.show()
vendas_por_mes['Vendas (em quantidade)'].describe()
count 33.000000 mean 4987.030303 std 159.145469 min 4487.000000 25% 4926.000000 50% 5025.000000 75% 5105.000000 max 5206.000000 Name: Vendas (em quantidade), dtype: float64
# Determinando outliers
Q1 = vendas_por_mes['Vendas (em quantidade)'].describe()[4]
Q3 = vendas_por_mes['Vendas (em quantidade)'].describe()[6]
IQR = Q3 - Q1
Limite_superior = Q3 + 1.5*IQR
Limite_inferior = Q1 - 1.5*IQR
print(f'Quantidade de vendas mensais maiores que {Limite_superior} são outliers superiores')
print(f'Quantidade de vendas mensais menores que {Limite_inferior} são outliers inferiores')
Quantidade de vendas mensais maiores que 5373.5 são outliers superiores Quantidade de vendas mensais menores que 4657.5 são outliers inferiores
Ou seja, há apenas 1 outlier (inferior), em fevereiro/2015, quando foram vendidas 4487 itens.
# Visualizando a distribuição da quantidade de vendas ao mês
plt.figure(figsize=(16, 9))
sns.histplot(vendas_por_mes['Vendas (em quantidade)'], bins=50, kde=True)
plt.title('Distribuição da quantidade de vendas')
plt.xlabel('Quantidade')
plt.ylabel('Frequência')
plt.show()
Em todo o período avaliado, a média de vendas ao mês está em torno de 4987, bem próxima da mediana que está em 5025. Ou seja, A quantidade de vendas está bem concentrada em torno da média, com um baixo desvio padrão. Como a média é maior que a mediana, trata-se de uma distribuição assimétrica à esquerda.
print(f"A assimetria da distribuição de quantidades assume valor: {skew(vendas_por_mes['Vendas (em quantidade)']):.2f}")
A assimetria da distribuição de quantidades assume valor: -1.32
# Visualização
fig = px.line(vendas_por_mes, x='order date (DateOrders)', y='Variação de Vendas',
title='Variação das vendas mensais - em quantidade', labels={'order date (DateOrders)': 'Data'})
fig.update_layout(width=1000, height=600)
fig.show()
Pelos gráficos acima, percebe-se que o mês de fevereiro é o pior mês para vendas, com as maiores variações negativas.
# Agrupando pelo dia da semana e contando as vendas
vendas_dia_da_semana = df_filtrado.groupby(df_filtrado['order date (DateOrders)'].dt.day_name()).size()
# Ordenar os resultados pelos dias da semana
ordem_semana = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
vendas_dia_da_semana = vendas_dia_da_semana.reindex(ordem_semana)
vendas_dia_da_semana = vendas_dia_da_semana.reset_index()
vendas_dia_da_semana.rename(columns={0:'Quantidade de Vendas', 'order date (DateOrders)':'Dia da Semana'}, inplace=True)
vendas_dia_da_semana
| Dia da Semana | Quantidade de Vendas | |
|---|---|---|
| 0 | Monday | 23336 |
| 1 | Tuesday | 23409 |
| 2 | Wednesday | 23272 |
| 3 | Thursday | 23598 |
| 4 | Friday | 23755 |
| 5 | Saturday | 23640 |
| 6 | Sunday | 23562 |
fig = px.bar(vendas_dia_da_semana, x='Dia da Semana', y='Quantidade de Vendas', color = 'Dia da Semana',
text = 'Quantidade de Vendas', title='Vendas por dia da semana - em quantidade')
fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()
Percebe-se que as quantidade de vendas por dia da semana assumem valores gerais bem próximos uns dos outros, não tendo um dia de preferencial de compras dos clientes.
6.2 Análise temporal das vendas - financeiro¶
# Agrupar os dados por ano e mês e somar as vendas
sales_over_time = df_filtrado.resample('M', on='order date (DateOrders)').sum()['Sales']
# Criando um DataFrame a partir da série
sales_over_time_df = sales_over_time.reset_index()
# Calculando a variação das vendas
sales_over_time_df['Variação de Vendas'] = sales_over_time_df['Sales'].diff()
# Calculando a variação percentual das vendas
sales_over_time_df['Variação Percentual'] = sales_over_time_df['Sales'].pct_change()
# Convertendo a variação para porcentagem e arredondando
sales_over_time_df['Variação Percentual'] = (sales_over_time_df['Variação Percentual'] * 100).round(2)
# Visualização
fig = px.line(sales_over_time_df, x='order date (DateOrders)', y='Sales',
title='Vendas Totais por Mês (em milhões)', labels={'order date (DateOrders)': 'Data', 'Sales': 'Vendas Totais (em milhões)'})
fig.update_layout(width=1000, height=600)
fig.show()
sales_over_time.describe()
count 3.300000e+01 mean 9.935833e+05 std 3.796040e+04 min 8.816670e+05 25% 9.784447e+05 50% 9.924190e+05 75% 1.007894e+06 max 1.088272e+06 Name: Sales, dtype: float64
As vendas variam em torno de uma média de 993 mil, terminando setembro/2017 na maior alta histórica, cerca de 1,08MM entre todos os produtos comercializados.
# Visualização
fig = px.line(sales_over_time_df, x='order date (DateOrders)', y='Variação de Vendas',
title='Variação das vendas Totais por Mês', labels={'order date (DateOrders)': 'Data', 'Variação de Vendas':'Variação de Vendas (em milhares)'})
fig.update_layout(width=1000, height=600)
fig.show()
sales_over_time_df[abs(sales_over_time_df['Variação Percentual']) >= 5]
| order date (DateOrders) | Sales | Variação de Vendas | Variação Percentual | |
|---|---|---|---|---|
| 1 | 2015-02-28 | 8.816670e+05 | -127187.442220 | -12.61 |
| 2 | 2015-03-31 | 1.007758e+06 | 126091.062438 | 14.30 |
| 13 | 2016-02-29 | 9.283721e+05 | -79433.621397 | -7.88 |
| 14 | 2016-03-31 | 9.879204e+05 | 59548.221787 | 6.41 |
| 26 | 2017-03-31 | 1.007894e+06 | 59852.532387 | 6.31 |
| 28 | 2017-05-31 | 1.047041e+06 | 55507.750888 | 5.60 |
| 29 | 2017-06-30 | 9.857683e+05 | -61272.661324 | -5.85 |
| 30 | 2017-07-31 | 1.047375e+06 | 61607.130510 | 6.25 |
Vendas por segmento de cliente¶
# Agrupando por 'Category Name' e somando as vendas
Venda_por_segmento = df_filtrado.groupby('Customer Segment')['Sales'].sum().reset_index()
Venda_por_segmento['Sales'] = (Venda_por_segmento['Sales'] / 1000000).round(2)
Venda_por_segmento.rename(columns={'Sales': 'Venda (em milhões)', 'Customer Segment': 'Segmento de Cliente'}, inplace=True)
# Ordenando os resultados
Venda_por_segmento = Venda_por_segmento.sort_values(by='Venda (em milhões)', ascending=False).reset_index(drop=True)
Venda_por_segmento
| Segmento de Cliente | Venda (em milhões) | |
|---|---|---|
| 0 | Consumer | 17.00 |
| 1 | Corporate | 9.96 |
| 2 | Home Office | 5.83 |
fig = px.bar(Venda_por_segmento, x='Segmento de Cliente', y='Venda (em milhões)', color = 'Segmento de Cliente',
text = 'Venda (em milhões)', title='Venda por segmento de cliente (em milhões)')
fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=500)
fig.show()
# Agora, agrupar os dados por categoria e data, somando o lucro
Venda_por_segmento_e_data = df_filtrado.groupby(['Customer Segment', pd.Grouper(key='order date (DateOrders)', freq='M')])['Sales'].sum().reset_index()
Venda_por_segmento_e_data['Sales'] = (Venda_por_segmento_e_data['Sales'] / 1000).round(2)
Venda_por_segmento_e_data.rename(columns={'Customer Segment':'Segmento', 'Sales': 'Vendas (em milhares)', 'order date (DateOrders)': 'Data'}, inplace=True)
# Calculando a variação das vendas por segmento
Venda_por_segmento_e_data['Variação de Vendas'] = Venda_por_segmento_e_data.groupby('Segmento')['Vendas (em milhares)'].diff()
# Calculando a variação percentual das vendas por segmento
Venda_por_segmento_e_data['Variação Percentual'] = Venda_por_segmento_e_data.groupby('Segmento')['Vendas (em milhares)'].pct_change()
# Convertendo a variação para porcentagem e arredondando
Venda_por_segmento_e_data['Variação Percentual'] = (Venda_por_segmento_e_data['Variação Percentual'] * 100).round(2)
# Criar o gráfico usando Plotly
fig = px.line(Venda_por_segmento_e_data, x='Data', y='Vendas (em milhares)', color='Segmento', title='Venda por segmento de cliente ao longo do tempo (em milhares)')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Criar o gráfico usando Plotly
fig = px.line(Venda_por_segmento_e_data, x='Data', y='Variação de Vendas', color='Segmento', title='Variação de venda por segmento de cliente ao longo do tempo')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Destacando os segmentos e os meses que tiveram uma variação maior que 10% nas vendas
Venda_por_segmento_e_data[abs(Venda_por_segmento_e_data['Variação Percentual']) >= 10]
| Segmento | Data | Vendas (em milhares) | Variação de Vendas | Variação Percentual | |
|---|---|---|---|---|---|
| 1 | Consumer | 2015-02-28 | 453.84 | -73.70 | -13.97 |
| 2 | Consumer | 2015-03-31 | 523.52 | 69.68 | 15.35 |
| 25 | Consumer | 2017-02-28 | 467.99 | -62.99 | -11.86 |
| 30 | Consumer | 2017-07-31 | 561.07 | 76.60 | 15.81 |
| 34 | Corporate | 2015-02-28 | 259.73 | -40.18 | -13.40 |
| 35 | Corporate | 2015-03-31 | 307.75 | 48.02 | 18.49 |
| 37 | Corporate | 2015-05-31 | 319.06 | 36.91 | 13.08 |
| 44 | Corporate | 2015-12-31 | 326.57 | 35.23 | 12.09 |
| 46 | Corporate | 2016-02-29 | 272.38 | -50.38 | -15.61 |
| 60 | Corporate | 2017-04-30 | 284.89 | -36.27 | -11.29 |
| 61 | Corporate | 2017-05-31 | 336.20 | 51.31 | 18.01 |
| 75 | Home Office | 2015-10-31 | 186.21 | 17.97 | 10.68 |
| 81 | Home Office | 2016-04-30 | 151.36 | -30.43 | -16.74 |
| 82 | Home Office | 2016-05-31 | 168.05 | 16.69 | 11.03 |
| 92 | Home Office | 2017-03-31 | 197.60 | 28.14 | 16.61 |
| 96 | Home Office | 2017-07-31 | 168.52 | -29.45 | -14.88 |
| 97 | Home Office | 2017-08-31 | 186.82 | 18.30 | 10.86 |
As vendas por seguimento oscilaram durante todo o período.
Destaque para os meses de variação positiva nas vendas:¶
- Corporate - Março/2015: 18,49%,
- Home Office - Março/2017: 16,61%
- Corporate - Março/2017: 18,01%
Destaque para os meses de variação negativa nas vendas:¶
- Home Office - Abril/2016: -16,74%
- Corporate - Fevereiro/2016: -15,61%
- Consumer - Fevereiro/2015: -13,97%
Categorias com mais vendas¶
# Agrupando por 'Category Name' e somando as vendas
vendas_por_categoria = df_filtrado.groupby('Category Name')['Sales'].sum().reset_index()
vendas_por_categoria['Sales'] = (vendas_por_categoria['Sales'] / 1000000).round(3)
vendas_por_categoria.rename(columns={'Sales': 'Vendas (em milhões)', 'Category Name': 'Nome da Categoria'}, inplace=True)
# Ordenando os resultados
vendas_por_categoria = vendas_por_categoria.sort_values(by='Vendas (em milhões)', ascending=False).reset_index(drop=True)
#sales_by_category_sorted = sales_by_category_sorted.head(10)
vendas_por_categoria
| Nome da Categoria | Vendas (em milhões) | |
|---|---|---|
| 0 | Fishing | 6.632 |
| 1 | Cleats | 4.237 |
| 2 | Camping & Hiking | 3.942 |
| 3 | Cardio Equipment | 3.527 |
| 4 | Women's Apparel | 3.008 |
| 5 | Water Sports | 2.977 |
| 6 | Indoor/Outdoor Games | 2.762 |
| 7 | Men's Footwear | 2.760 |
| 8 | Shop By Sport | 1.252 |
| 9 | Electronics | 0.357 |
| 10 | Girls' Apparel | 0.143 |
| 11 | Accessories | 0.128 |
| 12 | Golf Gloves | 0.112 |
| 13 | Golf Shoes | 0.104 |
| 14 | Baseball & Softball | 0.091 |
| 15 | Kids' Golf Clubs | 0.089 |
| 16 | Boxing & MMA | 0.082 |
| 17 | Golf Balls | 0.073 |
| 18 | Trade-In | 0.066 |
| 19 | Hunting & Shooting | 0.055 |
| 20 | Men's Golf Clubs | 0.046 |
| 21 | Hockey | 0.046 |
| 22 | Tennis & Racquet | 0.043 |
| 23 | Women's Golf Clubs | 0.042 |
| 24 | Lacrosse | 0.038 |
| 25 | Strength Training | 0.037 |
| 26 | Golf Apparel | 0.034 |
| 27 | Fitness Accessories | 0.033 |
| 28 | Soccer | 0.026 |
| 29 | As Seen on TV! | 0.020 |
| 30 | Basketball | 0.018 |
| 31 | Golf Bags & Carts | 0.010 |
fig = px.bar(vendas_por_categoria.head(10), x='Nome da Categoria', y='Vendas (em milhões)', color = 'Nome da Categoria',
text = 'Vendas (em milhões)', title='Top 10 categorias com mais vendas (em milhões)')
fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()
# Agora, agrupar os dados por categoria e data, somando o lucro
Vendas_por_categoria_e_data = df_filtrado.groupby(['Category Name', pd.Grouper(key='order date (DateOrders)', freq='M')])['Sales'].sum().reset_index()
Vendas_por_categoria_e_data['Sales'] = (Vendas_por_categoria_e_data['Sales'] / 1000).round(2)
Vendas_por_categoria_e_data.rename(columns={'Category Name':'Categoria', 'Sales': 'Vendas (em milhares)', 'order date (DateOrders)': 'Data'}, inplace=True)
# Calculando a variação das vendas por categoria
Vendas_por_categoria_e_data['Variação de Vendas'] = Vendas_por_categoria_e_data.groupby('Categoria')['Vendas (em milhares)'].diff()
# Calculando a variação percentual das vendas por categoria
Vendas_por_categoria_e_data['Variação Percentual'] = Vendas_por_categoria_e_data.groupby('Categoria')['Vendas (em milhares)'].pct_change()
# Convertendo a variação para porcentagem e arredondando
Vendas_por_categoria_e_data['Variação Percentual'] = (Vendas_por_categoria_e_data['Variação Percentual'] * 100).round(2)
# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_categoria_e_data, x='Data', y='Vendas (em milhares)', color='Categoria', title='Vendas por categoria ao longo do tempo')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_categoria_e_data, x='Data', y='Variação de Vendas', color='Categoria', title='Variação das vendas por categoria ao longo do tempo (em milhares)')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Destacando as categorias e os meses que tiveram uma variação maior que 500% nas vendas
Vendas_por_categoria_e_data[abs(Vendas_por_categoria_e_data['Variação Percentual']) > 500.00]
| Categoria | Data | Vendas (em milhares) | Variação de Vendas | Variação Percentual | |
|---|---|---|---|---|---|
| 289 | Fitness Accessories | 2017-09-30 | 4.20 | 3.92 | 1400.00 |
| 478 | Hockey | 2017-09-30 | 8.25 | 7.61 | 1189.06 |
| 667 | Strength Training | 2017-09-30 | 25.37 | 21.40 | 539.04 |
As vendas por categoria oscilaram durante todo o período.
Destaque para os meses de variação positiva nas vendas:¶
- Fitness Accessories - Setembro/2017: 1400%,
- Hockey - Setembro/2017: 1189,06%
- Strength Training - Setembro/2017: 539,04%
Vendas por departamento¶
# Agrupando por 'Category Name' e somando as vendas
Vendas_por_depart = df_filtrado.groupby('Department Name')['Sales'].sum().reset_index()
Vendas_por_depart['Sales'] = (Vendas_por_depart['Sales'] / 1000000).round(2)
Vendas_por_depart.rename(columns={'Sales': 'Vendas (em milhões)', 'Department Name': 'Departamento'}, inplace=True)
# Ordenando os resultados
Vendas_por_depart = Vendas_por_depart.sort_values(by='Vendas (em milhões)', ascending=False).reset_index(drop=True)
Vendas_por_depart
| Departamento | Vendas (em milhões) | |
|---|---|---|
| 0 | Fan Shop | 16.37 |
| 1 | Apparel | 7.00 |
| 2 | Golf | 4.40 |
| 3 | Footwear | 3.81 |
| 4 | Outdoors | 0.95 |
| 5 | Fitness | 0.26 |
fig = px.bar(Vendas_por_depart, x='Departamento', y='Vendas (em milhões)', color = 'Departamento',
text = 'Vendas (em milhões)', title='Vendas por departamento (em milhões)')
fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()
# Agora, agrupar os dados por categoria e data, somando o lucro
Vendas_por_depart_e_data = df_filtrado.groupby(['Department Name', pd.Grouper(key='order date (DateOrders)', freq='M')])['Sales'].sum().reset_index()
Vendas_por_depart_e_data['Sales'] = (Vendas_por_depart_e_data['Sales'] / 1000).round(2)
Vendas_por_depart_e_data.rename(columns={'Department Name':'Departamento', 'Sales': 'Vendas (em milhares)', 'order date (DateOrders)': 'Data'}, inplace=True)
# Calculando a variação das vendas por departamento
Vendas_por_depart_e_data['Variação de Vendas'] = Vendas_por_depart_e_data.groupby('Departamento')['Vendas (em milhares)'].diff()
# Calculando a variação percentual das vendas por departamento
Vendas_por_depart_e_data['Variação Percentual'] = Vendas_por_depart_e_data.groupby('Departamento')['Vendas (em milhares)'].pct_change()
# Convertendo a variação para porcentagem e arredondando
Vendas_por_depart_e_data['Variação Percentual'] = (Vendas_por_depart_e_data['Variação Percentual'] * 100).round(2)
# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_depart_e_data, x='Data', y='Vendas (em milhares)', color='Departamento', title='Vendas por departamento ao Longo do Tempo (em milhares)')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_depart_e_data, x='Data', y='Variação de Vendas', color='Departamento', title='Variação das vendas por departamento ao Longo do Tempo (em milhares)')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Destacando os departamentos e os meses que tiveram uma variação maior que 50% nas vendas
Vendas_por_depart_e_data[abs(Vendas_por_depart_e_data['Variação Percentual']) > 50.00]
| Departamento | Data | Vendas (em milhares) | Variação de Vendas | Variação Percentual | |
|---|---|---|---|---|---|
| 77 | Fitness | 2015-12-31 | 7.83 | 2.63 | 50.58 |
| 84 | Fitness | 2016-07-31 | 8.87 | 3.61 | 68.63 |
| 94 | Fitness | 2017-05-31 | 13.82 | 6.29 | 83.53 |
| 98 | Fitness | 2017-09-30 | 30.07 | 19.21 | 176.89 |
| 193 | Outdoors | 2017-05-31 | 62.28 | 32.88 | 111.84 |
As vendas por departamento oscilaram durante todo o período.
Destaque para os meses de variação positiva nas vendas:¶
- Fitness - Setembro/2017: 176,89%,
- Outdoors - Maio/2017: 111,84%
Vendas por Mercado Global¶
# Agrupando por 'Category Name' e somando as vendas
vendas_por_market = df_filtrado.groupby('Market')['Sales'].sum().reset_index()
vendas_por_market['Sales'] = (vendas_por_market['Sales'] / 1000000).round(2)
vendas_por_market.rename(columns={'Sales': 'Vendas (em milhões)', 'Market': 'Mercado Global'}, inplace=True)
# Ordenando os resultados
vendas_por_market = vendas_por_market.sort_values(by='Vendas (em milhões)', ascending=False).reset_index(drop=True)
vendas_por_market
| Mercado Global | Vendas (em milhões) | |
|---|---|---|
| 0 | LATAM | 9.82 |
| 1 | Europe | 9.19 |
| 2 | Pacific Asia | 6.74 |
| 3 | USCA | 4.84 |
| 4 | Africa | 2.21 |
fig = px.bar(vendas_por_market, x='Mercado Global', y='Vendas (em milhões)', color = 'Mercado Global',
text = 'Vendas (em milhões)', title='Total em vendas por Mercado Global (em milhões)')
fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()
# Agora, agrupar os dados por categoria e data, somando o lucro
Vendas_por_market_e_data = df_filtrado.groupby(['Market', pd.Grouper(key='order date (DateOrders)', freq='M')])['Sales'].sum().reset_index()
Vendas_por_market_e_data['Sales'] = (Vendas_por_market_e_data['Sales'] / 1000000).round(2)
Vendas_por_market_e_data.rename(columns={'Market':'Mercado Global', 'Sales': 'Vendas (em milhões)', 'order date (DateOrders)': 'Data'}, inplace=True)
# Calculando a variação das vendas por mercado global
Vendas_por_market_e_data['Variação de Vendas'] = Vendas_por_market_e_data.groupby('Mercado Global')['Vendas (em milhões)'].diff()
# Calculando a variação percentual das vendas por mercado global
Vendas_por_market_e_data['Variação Percentual'] = Vendas_por_market_e_data.groupby('Mercado Global')['Vendas (em milhões)'].pct_change()
# Convertendo a variação para porcentagem e arredondando
Vendas_por_market_e_data['Variação Percentual'] = (Vendas_por_market_e_data['Variação Percentual'] * 100).round(2)
# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_market_e_data, x='Data', y='Vendas (em milhões)', color='Mercado Global', title='Vendas por Mercado Global ao longo do tempo (em milhões)')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_market_e_data, x='Data', y='Variação de Vendas', color='Mercado Global', title='Variação de vendas por Mercado Global ao longo do tempo (em milhões)')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Destacando os mercados globais e os meses que tiveram uma variação maior que 200% nas vendas
Vendas_por_market_e_data[abs(Vendas_por_market_e_data['Variação Percentual']) > 200.00]
| Mercado Global | Data | Vendas (em milhões) | Variação de Vendas | Variação Percentual | |
|---|---|---|---|---|---|
| 1 | Africa | 2016-09-30 | 0.49 | 0.40 | 444.44 |
| 7 | Europe | 2015-06-30 | 0.98 | 0.96 | 4800.00 |
| 13 | Europe | 2016-09-30 | 0.16 | 0.13 | 433.33 |
| 18 | Europe | 2017-06-30 | 0.43 | 0.32 | 290.91 |
| 34 | Pacific Asia | 2015-11-30 | 0.99 | 0.73 | 280.77 |
| 41 | Pacific Asia | 2016-09-30 | 0.29 | 0.21 | 262.50 |
As vendas por seguimento oscilaram durante todo o período.
Destaque para os meses de variação positiva nas vendas:¶
- Europe - Junho/2015: 4800% - Junho e setembro foram meses bem favoráveis para o mercado Europeu.
- Pacific Asia - Novembro/2015: 280,77% - Para este mercado, os meses novembro e setembro foram bem positivos.
Vendas por região do cliente¶
# Agrupando por 'Category Name' e somando as vendas
Vendas_por_regiao_do_cliente = df_filtrado.groupby('Customer Country')['Sales'].sum().reset_index()
Vendas_por_regiao_do_cliente['Sales'] = (Vendas_por_regiao_do_cliente['Sales'] / 1000000).round(2)
Vendas_por_regiao_do_cliente.rename(columns={'Sales': 'Vendas (em milhões)', 'Customer Country': 'País de origem do cliente'}, inplace=True)
# Ordenando os resultados
Vendas_por_regiao_do_cliente = Vendas_por_regiao_do_cliente.sort_values(by='Vendas (em milhões)', ascending=False).reset_index(drop=True)
Vendas_por_regiao_do_cliente
| País de origem do cliente | Vendas (em milhões) | |
|---|---|---|
| 0 | EE. UU. | 20.18 |
| 1 | Puerto Rico | 12.61 |
fig = px.bar(Vendas_por_regiao_do_cliente, x='País de origem do cliente', y='Vendas (em milhões)', color = 'País de origem do cliente',
text = 'Vendas (em milhões)', title='Vendas por região de origem do cliente (em milhões)')
fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=500)
fig.show()
Os clientes da empresa são basicamente de duas nacionalidades: Estados Unidos e Porto Rico.
# Agora, agrupar os dados por categoria e data, somando o lucro
Vendas_por_regiao_do_cliente_e_data = df_filtrado.groupby(['Customer Country', pd.Grouper(key='order date (DateOrders)', freq='M')])['Sales'].sum().reset_index()
Vendas_por_regiao_do_cliente_e_data['Sales'] = (Vendas_por_regiao_do_cliente_e_data['Sales'] / 1000000).round(2)
Vendas_por_regiao_do_cliente_e_data.rename(columns={'Customer Country':'País de origem do cliente', 'Sales': 'Vendas (em milhoes)', 'order date (DateOrders)': 'Data'}, inplace=True)
# Calculando a variação das vendas por regiao do cliente
Vendas_por_regiao_do_cliente_e_data['Variação de Vendas'] = Vendas_por_regiao_do_cliente_e_data.groupby('País de origem do cliente')['Vendas (em milhoes)'].diff()
# Calculando a variação percentual das vendas por regiao do cliente
Vendas_por_regiao_do_cliente_e_data['Variação Percentual'] = Vendas_por_regiao_do_cliente_e_data.groupby('País de origem do cliente')['Vendas (em milhoes)'].pct_change()
# Convertendo a variação para porcentagem e arredondando
Vendas_por_regiao_do_cliente_e_data['Variação Percentual'] = (Vendas_por_regiao_do_cliente_e_data['Variação Percentual'] * 100).round(2)
# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_regiao_do_cliente_e_data, x='Data', y='Vendas (em milhoes)', color='País de origem do cliente', title='Vendas por região do cliente ao Longo do Tempo (em milhoes)')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_regiao_do_cliente_e_data, x='Data', y='Variação de Vendas', color='País de origem do cliente', title='Variação de vendas por região do cliente ao Longo do Tempo (em milhoes)')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Destacando os mercados globais e os meses que tiveram uma variação maior que 10% nas vendas
Vendas_por_regiao_do_cliente_e_data[abs(Vendas_por_regiao_do_cliente_e_data['Variação Percentual']) > 10.00]
| País de origem do cliente | Data | Vendas (em milhoes) | Variação de Vendas | Variação Percentual | |
|---|---|---|---|---|---|
| 1 | EE. UU. | 2015-02-28 | 0.54 | -0.09 | -14.29 |
| 2 | EE. UU. | 2015-03-31 | 0.62 | 0.08 | 14.81 |
| 13 | EE. UU. | 2016-02-29 | 0.57 | -0.08 | -12.31 |
| 21 | EE. UU. | 2016-10-31 | 0.65 | 0.06 | 10.17 |
| 28 | EE. UU. | 2017-05-31 | 0.67 | 0.09 | 15.52 |
| 29 | EE. UU. | 2017-06-30 | 0.60 | -0.07 | -10.45 |
| 34 | Puerto Rico | 2015-02-28 | 0.34 | -0.04 | -10.53 |
| 35 | Puerto Rico | 2015-03-31 | 0.39 | 0.05 | 14.71 |
| 49 | Puerto Rico | 2016-05-31 | 0.40 | 0.06 | 17.65 |
| 59 | Puerto Rico | 2017-03-31 | 0.39 | 0.04 | 11.43 |
| 63 | Puerto Rico | 2017-07-31 | 0.42 | 0.04 | 10.53 |
Durante o período avaliado, percebe-se que:
- Os Estados Unidos tiveram variações mais significativas:: Fevereiro/2015 (-14,29%), Março/2015 (14,81%) e Maio/2017 (15,52%)
- Puerto Rico teve variações mais significativas: Maio/2015 (17,65%), Março/2015 (14,71%) e Março/2017 (11,43%)
Vendas por região de destino¶
# Agrupando por 'Category Name' e somando as vendas
Vendas_por_destino = df_filtrado.groupby('Order Region')['Sales'].sum().reset_index()
Vendas_por_destino['Sales'] = (Vendas_por_destino['Sales'] / 1000000).round(2)
Vendas_por_destino.rename(columns={'Order Region':'Região de destino', 'Sales': 'Vendas (em milhões)'}, inplace=True)
# Ordenando os resultados
Vendas_por_destino = Vendas_por_destino.sort_values(by='Vendas (em milhões)', ascending=False).reset_index(drop=True)
Vendas_por_destino
| Região de destino | Vendas (em milhões) | |
|---|---|---|
| 0 | Central America | 5.43 |
| 1 | Western Europe | 4.92 |
| 2 | South America | 2.81 |
| 3 | Northern Europe | 1.80 |
| 4 | Southern Europe | 1.71 |
| 5 | Oceania | 1.65 |
| 6 | Caribbean | 1.58 |
| 7 | West of USA | 1.49 |
| 8 | Southeast Asia | 1.47 |
| 9 | East of USA | 1.31 |
| 10 | South Asia | 1.25 |
| 11 | Eastern Asia | 1.12 |
| 12 | West Asia | 1.12 |
| 13 | US Center | 1.11 |
| 14 | South of USA | 0.75 |
| 15 | Eastern Europe | 0.75 |
| 16 | West Africa | 0.70 |
| 17 | North Africa | 0.61 |
| 18 | East Africa | 0.36 |
| 19 | Central Africa | 0.32 |
| 20 | Southern Africa | 0.22 |
| 21 | Canada | 0.18 |
| 22 | Central Asia | 0.11 |
fig = px.bar(Vendas_por_destino.head(10), x='Região de destino', y='Vendas (em milhões)', color = 'Região de destino',
text = 'Vendas (em milhões)', title='Top 10 regiões de destino com mais vendas (em milhões)')
fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=500)
fig.show()
# Obtendo a lista das regiões mais lucrativas no geral
top_5_regions = Vendas_por_destino.head(5)['Região de destino'].tolist()
# Agora, agrupar os dados por categoria e data, somando o lucro
Vendas_por_destino_e_data = df_filtrado.groupby(['Order Region', pd.Grouper(key='order date (DateOrders)', freq='M')])['Sales'].sum().reset_index()
Vendas_por_destino_e_data['Sales'] = (Vendas_por_destino_e_data['Sales'] / 1000000).round(2)
Vendas_por_destino_e_data.rename(columns={'Order Region':'Região de destino', 'Sales': 'Vendas (em milhões)', 'order date (DateOrders)':'Data'}, inplace=True)
# Calculando a variação das vendas por regiao de destino da entrega
Vendas_por_destino_e_data['Variação de Vendas'] = Vendas_por_destino_e_data.groupby('Região de destino')['Vendas (em milhões)'].diff()
# Calculando a variação percentual das vendas por regiao de destino da entrega
Vendas_por_destino_e_data['Variação Percentual'] = Vendas_por_destino_e_data.groupby('Região de destino')['Vendas (em milhões)'].pct_change()
# Convertendo a variação para porcentagem e arredondando
Vendas_por_destino_e_data['Variação Percentual'] = (Vendas_por_destino_e_data['Variação Percentual'] * 100).round(2)
# Filtrando do dataframe 'Lucro_por_regiao_e_data' somente as regiões do 'top_5_regions'
Vendas_por_destino_e_data_filtrado = Vendas_por_destino_e_data[Vendas_por_destino_e_data['Região de destino'].isin(top_5_regions)]
# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_destino_e_data_filtrado, x='Data', y='Vendas (em milhões)', color='Região de destino', title='Top 5 das regiões de destino com mais vendas ao longo do tempo (em milhões)')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Criar o gráfico usando Plotly
fig = px.line(Vendas_por_destino_e_data_filtrado, x='Data', y='Variação de Vendas', color='Região de destino', title='Variação das vendas das Top 5 regiões que mais venderam (em milhões)')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Destacando os destinos das entregas e os meses que tiveram uma variação maior que 1000% nas vendas
Vendas_por_destino_e_data[~Vendas_por_destino_e_data['Variação Percentual'].isin([np.inf, -np.inf]) &
(abs(Vendas_por_destino_e_data['Variação Percentual']) > 500.00)]
| Região de destino | Data | Vendas (em milhões) | Variação de Vendas | Variação Percentual | |
|---|---|---|---|---|---|
| 18 | Central Africa | 2016-09-30 | 0.07 | 0.06 | 600.0 |
| 41 | East Africa | 2016-09-30 | 0.08 | 0.07 | 700.0 |
| 63 | Eastern Europe | 2016-09-30 | 0.14 | 0.12 | 600.0 |
| 75 | Northern Europe | 2015-06-30 | 0.19 | 0.18 | 1800.0 |
| 86 | Northern Europe | 2017-06-30 | 0.08 | 0.07 | 700.0 |
| 150 | Southern Europe | 2017-06-30 | 0.07 | 0.06 | 600.0 |
| 160 | West Africa | 2016-09-30 | 0.15 | 0.13 | 650.0 |
| 177 | Western Europe | 2015-06-30 | 0.59 | 0.58 | 5800.0 |
As vendas de acordo com a região de destino oscilaram durante todo o período.
Destaque para os meses de variação positiva nas vendas:¶
Northern Europe - Junho/2015: 1800%
Western Europe - Junho/2015: 5800%
Regiões da Europa como um todo tiveram bons desempenhos nos meses de junho e setembro
6.3 Análise de Lucratividade¶
# Criando uma nova coluna, margem de lucro = (Benefício por pedido / Vendas) * 100
df_filtrado['Profit Margin'] = (df_filtrado['Benefit per order'] / df_filtrado['Sales']) * 100
# Visualizando a distribuição da margem de lucro
plt.figure(figsize=(16, 9))
sns.histplot(df_filtrado['Profit Margin'], bins=30, kde=True)
plt.title('Distribuição da Margem de Lucro')
plt.xlabel('Margem de Lucro (%)')
plt.ylabel('Frequência')
plt.show()
df_filtrado['Benefit per order'].describe()
count 164572.000000 mean 21.449250 std 97.492779 min -1844.979980 25% 7.420000 50% 31.670000 75% 63.860001 max 721.599976 Name: Benefit per order, dtype: float64
fig = sns.boxplot(y='Benefit per order', data=df_filtrado)
plt.xticks(rotation=90)
([0], [Text(0, 0, '')])
O boxplot acima mostra que a mediana, Q1 (primeiro quartil) e Q3 (terceiro quartil) estão muito próximos a zero, ou seja, para grande maioria dos casos não há uma margem de lucro positiva significante na venda dos produtos, mesmo com os outliers acima do limite superior. Além disso, há outliers bem abaixo do limite inferior e bem abaixo de zero, o que indica que a distribuição é bem assimétrica à esquerda e também que grande parte das vendas gera prejuízo à empresa.
# Agrupar os dados por ano e mês e somar as vendas
benefit_over_time = df_filtrado.resample('M', on='order date (DateOrders)').sum()['Benefit per order']
# Criar um DataFrame a partir da série
benefit_over_time_df = benefit_over_time.reset_index()
# Calculando a variação das vendas
benefit_over_time_df['Variação do Lucro'] = benefit_over_time_df['Benefit per order'].diff()
# Calculando a variação percentual das vendas
benefit_over_time_df['Variação Percentual'] = benefit_over_time_df['Benefit per order'].pct_change()
# Convertendo a variação para porcentagem e arredondando
benefit_over_time_df['Variação Percentual'] = (benefit_over_time_df['Variação Percentual'] * 100).round(2)
# Visualização
fig = px.line(benefit_over_time_df, x='order date (DateOrders)', y='Benefit per order',
title='Lucros Totais por Mês', labels={'order date (DateOrders)': 'Data', 'Benefit per order': 'Lucros Totais'})
fig.show()
benefit_over_time.describe()
count 33.000000 mean 106968.057397 std 8226.151101 min 83038.990604 25% 102710.810232 50% 106958.890036 75% 111232.069980 max 126668.110171 Name: Benefit per order, dtype: float64
Os lucros variam em torno de uma média de 107 mil, atingindo a máxima histórica com 126 mil, no mês de agosto de 2017.
# Visualização
fig = px.line(benefit_over_time_df, x='order date (DateOrders)', y='Variação do Lucro',
title='Variação dos Lucros Totais por Mês', labels={'order date (DateOrders)': 'Data'})
fig.update_layout(width=1000, height=500)
fig.show()
# Destacando os meses que tiveram uma variação maior que 10% nos lucros da empresa
benefit_over_time_df[abs(benefit_over_time_df['Variação Percentual']) > 10.00]
| order date (DateOrders) | Benefit per order | Variação do Lucro | Variação Percentual | |
|---|---|---|---|---|
| 1 | 2015-02-28 | 93009.960174 | -12763.610006 | -12.07 |
| 2 | 2015-03-31 | 107401.920248 | 14391.960074 | 15.47 |
| 9 | 2015-10-31 | 96839.170095 | -12760.050088 | -11.64 |
| 13 | 2016-02-29 | 83038.990604 | -19671.819628 | -19.15 |
| 14 | 2016-03-31 | 97525.060235 | 14486.069631 | 17.44 |
| 15 | 2016-04-30 | 108641.619867 | 11116.559631 | 11.40 |
| 18 | 2016-07-31 | 114827.080170 | 12173.540082 | 11.86 |
| 20 | 2016-09-30 | 121734.689953 | 17168.649881 | 16.42 |
| 24 | 2017-01-31 | 113146.160262 | 14171.800296 | 14.32 |
| 31 | 2017-08-31 | 126668.110171 | 15436.040191 | 13.88 |
A lucratividade oscilou durante todo o período avaliado, mas com destaque para as variações:
- Fevereiro/2016: -19,15% e Fevereiro/2015: -12,07%,
- Março/2016: 16,42% e Março/2015: 15,47%
Lucratividade por categoria¶
# Agrupando por 'Category Name' e somando as vendas
Lucro_por_categoria = df_filtrado.groupby('Category Name')['Benefit per order'].sum().reset_index()
Lucro_por_categoria['Benefit per order'] = (Lucro_por_categoria['Benefit per order'] / 1000).round(2)
Lucro_por_categoria.rename(columns={'Category Name':'Categoria','Benefit per order': 'Lucro (em milhares)'}, inplace=True)
# Ordenando os resultados
Lucro_por_categoria = Lucro_por_categoria.sort_values(by='Lucro (em milhares)', ascending=False).reset_index(drop=True)
Lucro_por_categoria
| Categoria | Lucro (em milhares) | |
|---|---|---|
| 0 | Fishing | 730.16 |
| 1 | Cleats | 473.68 |
| 2 | Camping & Hiking | 409.22 |
| 3 | Cardio Equipment | 361.56 |
| 4 | Women's Apparel | 334.16 |
| 5 | Water Sports | 308.59 |
| 6 | Indoor/Outdoor Games | 302.19 |
| 7 | Men's Footwear | 296.36 |
| 8 | Shop By Sport | 124.81 |
| 9 | Electronics | 38.51 |
| 10 | Girls' Apparel | 16.92 |
| 11 | Accessories | 15.75 |
| 12 | Golf Gloves | 13.65 |
| 13 | Baseball & Softball | 12.24 |
| 14 | Golf Shoes | 11.60 |
| 15 | Kids' Golf Clubs | 9.51 |
| 16 | Boxing & MMA | 7.93 |
| 17 | Golf Balls | 7.57 |
| 18 | Trade-In | 7.36 |
| 19 | Tennis & Racquet | 5.97 |
| 20 | Hockey | 5.97 |
| 21 | Hunting & Shooting | 5.88 |
| 22 | Women's Golf Clubs | 5.37 |
| 23 | Men's Golf Clubs | 5.28 |
| 24 | Fitness Accessories | 5.06 |
| 25 | Lacrosse | 4.10 |
| 26 | Soccer | 3.85 |
| 27 | Golf Apparel | 3.40 |
| 28 | Golf Bags & Carts | 1.81 |
| 29 | As Seen on TV! | 1.45 |
| 30 | Strength Training | 0.48 |
| 31 | Basketball | -0.44 |
fig = px.bar(Lucro_por_categoria.head(10), x='Categoria', y='Lucro (em milhares)', color = 'Categoria',
text = 'Lucro (em milhares)', title='Top 10 categorias mais lucrativas (em milhares)')
fig.update_traces(textposition='outside', texttemplate='%{text}k', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()
# Agora, agrupar os dados por categoria e data, somando o lucro
Lucro_por_categoria_e_data = df_filtrado.groupby(['Category Name', pd.Grouper(key='order date (DateOrders)', freq='M')])['Benefit per order'].sum().reset_index()
Lucro_por_categoria_e_data.rename(columns={'Category Name':'Categoria', 'Benefit per order': 'Lucro (em milhares)', 'order date (DateOrders)': 'Data'}, inplace=True)
# Calculando a variação dos lucros por categoria
Lucro_por_categoria_e_data['Variação de Lucro'] = Lucro_por_categoria_e_data.groupby('Categoria')['Lucro (em milhares)'].diff()
# Calculando a variação percentual dos lucros por categoria
Lucro_por_categoria_e_data['Variação Percentual'] = Lucro_por_categoria_e_data.groupby('Categoria')['Lucro (em milhares)'].pct_change()
# Convertendo a variação para porcentagem e arredondando
Lucro_por_categoria_e_data['Variação Percentual'] = (Lucro_por_categoria_e_data['Variação Percentual'] * 100).round(2)
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_categoria_e_data, x='Data', y='Lucro (em milhares)', color='Categoria', title='Lucro por categoria ao longo do tempo (em milhares)')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_categoria_e_data, x='Data', y='Variação de Lucro', color='Categoria', title='Variação no lucro por categoria ao longo do tempo (em milhares)')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Destacando os meses em que as categorias tiveram uma variação maior que 2000% nos lucros da empresa
Lucro_por_categoria_e_data[abs(Lucro_por_categoria_e_data['Variação Percentual']) > 2000.00]
| Categoria | Data | Lucro (em milhares) | Variação de Lucro | Variação Percentual | |
|---|---|---|---|---|---|
| 289 | Fitness Accessories | 2017-09-30 | 961.479996 | 954.089994 | 12910.55 |
| 352 | Golf Bags & Carts | 2017-05-31 | 723.549999 | 761.370005 | -2013.14 |
| 478 | Hockey | 2017-09-30 | 1241.189989 | 1183.699989 | 2058.97 |
| 483 | Hunting & Shooting | 2015-05-31 | -407.849995 | -421.079991 | -3182.77 |
| 657 | Soccer | 2017-05-31 | 1448.839995 | 1432.999994 | 9046.72 |
| 711 | Trade-In | 2016-04-30 | 432.400003 | 436.499995 | -10646.36 |
Os lucros oscilaram durante todo o período.
Destaque para os meses de variação positiva nos lucros:¶
- Fitness Accessories - Setembro/2017: 12910,55%
- Soccer - Maio/2017: 6,41%
- Hockey - Setembro/2017: 2058,97%
Destaque para os meses de variação negativa nos lucros:¶
- Trade-In - Abril/2016: -10646,36%,
- Hunting & Shooting - Maio/2015: -3182,77%
- Golf Bags & Carts - Maio/2017: -2013,14%
sns.set(rc = {'figure.figsize':(23,11)})
fig = sns.boxplot(x='Category Name', y='Benefit per order', data=df_filtrado)
plt.xticks(rotation=90);
Lucratividade por Mercado Global¶
# Agrupando por 'Category Name' e somando as vendas
Lucro_por_market = df_filtrado.groupby('Market')['Benefit per order'].sum().reset_index()
Lucro_por_market['Benefit per order'] = (Lucro_por_market['Benefit per order'] / 1000000).round(2)
Lucro_por_market.rename(columns={'Benefit per order': 'Lucro (em milhões)'}, inplace=True)
# Ordenando os resultados
Lucro_por_market = Lucro_por_market.sort_values(by='Lucro (em milhões)', ascending=False).reset_index(drop=True)
Lucro_por_market
| Market | Lucro (em milhões) | |
|---|---|---|
| 0 | LATAM | 1.07 |
| 1 | Europe | 1.00 |
| 2 | Pacific Asia | 0.68 |
| 3 | USCA | 0.54 |
| 4 | Africa | 0.24 |
fig = px.bar(Lucro_por_market, x='Market', y='Lucro (em milhões)', color = 'Market',
text = 'Lucro (em milhões)', title='Lucro total por Mercado Global (em milhões)')
fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()
# Agora, agrupar os dados por categoria e data, somando o lucro
Lucro_por_market_e_data = df_filtrado.groupby(['Market', pd.Grouper(key='order date (DateOrders)', freq='M')])['Benefit per order'].sum().reset_index()
Lucro_por_market_e_data.rename(columns={'Market':'Mercado Global', 'Benefit per order': 'Lucro (em milhares)', 'order date (DateOrders)': 'Data'}, inplace=True)
# Calculando a variação dos lucros por mercado global
Lucro_por_market_e_data['Variação de Lucro'] = Lucro_por_market_e_data.groupby('Mercado Global')['Lucro (em milhares)'].diff()
# Calculando a variação percentual dos lucros por mercado global
Lucro_por_market_e_data['Variação Percentual'] = Lucro_por_market_e_data.groupby('Mercado Global')['Lucro (em milhares)'].pct_change()
# Convertendo a variação para porcentagem e arredondando
Lucro_por_market_e_data['Variação Percentual'] = (Lucro_por_market_e_data['Variação Percentual'] * 100).round(2)
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_market_e_data, x='Data', y='Lucro (em milhares)', color='Mercado Global', title='Lucro por Mercado Global ao Longo do Tempo (em milhares)')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_market_e_data, x='Data', y='Variação de Lucro', color='Mercado Global', title='Variação no lucro por Mercado Global ao Longo do Tempo (em milhares)')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Destacando os meses em que mercados globais tiveram uma variação maior que 1000% nos lucros da empresa
Lucro_por_market_e_data[abs(Lucro_por_market_e_data['Variação Percentual']) > 1000.00]
| Mercado Global | Data | Lucro (em milhares) | Variação de Lucro | Variação Percentual | |
|---|---|---|---|---|---|
| 7 | Europe | 2015-06-30 | 105576.890319 | 102701.120345 | 3571.26 |
| 13 | Europe | 2016-09-30 | 19998.460034 | 18804.170045 | 1574.51 |
| 41 | Pacific Asia | 2016-09-30 | 36197.930071 | 34461.110042 | 1984.15 |
Os lucros oscilaram durante todo o período.
Destaque para os meses de variação positiva nos lucros:¶
- Europe - junho/2015: 3571,26%
- Pacific Asia - setembro/2016: 1984,15%
sns.set(rc = {'figure.figsize':(16,9)})
fig = sns.boxplot(x='Market', y='Benefit per order', data=df_filtrado)
plt.xticks(rotation=90);
Regiões de destino mais lucrativos para a empresa¶
# Agrupando por 'Category Name' e somando as vendas
Lucro_por_destino = df_filtrado.groupby('Order Region')['Benefit per order'].sum().reset_index()
Lucro_por_destino['Benefit per order'] = (Lucro_por_destino['Benefit per order'] / 1000).round(2)
Lucro_por_destino.rename(columns={'Order Region':'Região de destino', 'Benefit per order': 'Lucro (em milhares)'}, inplace=True)
# Ordenando os resultados
Lucro_por_destino = Lucro_por_destino.sort_values(by='Lucro (em milhares)', ascending=False).reset_index(drop=True)
Lucro_por_destino
| Região de destino | Lucro (em milhares) | |
|---|---|---|
| 0 | Central America | 586.57 |
| 1 | Western Europe | 531.38 |
| 2 | South America | 315.50 |
| 3 | Northern Europe | 197.23 |
| 4 | Southern Europe | 194.95 |
| 5 | Caribbean | 166.98 |
| 6 | Oceania | 166.39 |
| 7 | West of USA | 159.19 |
| 8 | Southeast Asia | 150.32 |
| 9 | East of USA | 148.77 |
| 10 | South Asia | 129.39 |
| 11 | US Center | 126.36 |
| 12 | West Asia | 115.46 |
| 13 | Eastern Asia | 107.08 |
| 14 | South of USA | 82.37 |
| 15 | West Africa | 77.12 |
| 16 | Eastern Europe | 76.85 |
| 17 | North Africa | 59.36 |
| 18 | East Africa | 41.89 |
| 19 | Central Africa | 32.43 |
| 20 | Southern Africa | 29.24 |
| 21 | Canada | 22.54 |
| 22 | Central Asia | 12.58 |
fig = px.bar(Lucro_por_destino.head(10), x='Região de destino', y='Lucro (em milhares)', color = 'Região de destino',
text = 'Lucro (em milhares)', title='Top 10 regiões de destino mais lucrativas para a empresa (em milhares)')
fig.update_traces(textposition='outside', texttemplate='%{text}k', textfont_size=12)
fig.update_layout(width=1000, height=500)
fig.show()
# Obtendo a lista das regiões mais lucrativas no geral
top_5_regions = Lucro_por_destino.head(5)['Região de destino'].tolist()
# Agora, agrupar os dados por categoria e data, somando o lucro
Lucro_por_destino_e_data = df_filtrado.groupby(['Order Region', pd.Grouper(key='order date (DateOrders)', freq='M')])['Benefit per order'].sum().reset_index()
Lucro_por_destino_e_data.rename(columns={'Order Region':'Região de destino', 'Benefit per order': 'Lucro (em milhares)', 'order date (DateOrders)':'Data'}, inplace=True)
# Calculando a variação dos lucros por destino
Lucro_por_destino_e_data['Variação de Lucro'] = Lucro_por_destino_e_data.groupby('Região de destino')['Lucro (em milhares)'].diff()
# Calculando a variação percentual dos lucros por destino
Lucro_por_destino_e_data['Variação Percentual'] = Lucro_por_destino_e_data.groupby('Região de destino')['Lucro (em milhares)'].pct_change()
# Convertendo a variação para porcentagem e arredondando
Lucro_por_destino_e_data['Variação Percentual'] = (Lucro_por_destino_e_data['Variação Percentual'] * 100).round(2)
# Filtrando do dataframe 'Lucro_por_regiao_e_data' somente as regiões do 'top_5_regions'
Lucro_por_destino_e_data_filtrado = Lucro_por_destino_e_data[Lucro_por_destino_e_data['Região de destino'].isin(top_5_regions)]
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_destino_e_data_filtrado, x='Data', y='Lucro (em milhares)', color='Região de destino', title='Lucro das 5 regiões de destino mais rentáveis ao longo do tempo (em milhares)')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_destino_e_data_filtrado, x='Data', y='Variação de Lucro', color='Região de destino', title='Variação do lucro das 5 regiões de destino mais rentáveis ao longo do tempo')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Destacando os meses em que destinos tiveram uma variação maior que 2000% nos lucros da empresa
Lucro_por_destino_e_data[abs(Lucro_por_destino_e_data['Variação Percentual']) > 2000.00]
| Região de destino | Data | Lucro (em milhares) | Variação de Lucro | Variação Percentual | |
|---|---|---|---|---|---|
| 81 | Northern Europe | 2016-09-30 | 368.500006 | 383.520006 | -2553.40 |
| 115 | South Asia | 2016-09-30 | 7493.699992 | 7542.719976 | -15387.03 |
| 139 | Southern Europe | 2015-06-30 | 24983.670027 | 24487.640023 | 4936.73 |
| 150 | Southern Europe | 2017-06-30 | 7034.129999 | 7062.090028 | -25257.81 |
| 177 | Western Europe | 2015-06-30 | 56169.680160 | 55215.900179 | 5789.17 |
| 188 | Western Europe | 2017-06-30 | 28751.510036 | 28514.820043 | 12047.33 |
Os lucros oscilaram durante todo o período.
Destaque para os meses de variação positiva nos lucros:¶
- Western Europe - junho/2017: 12047,33%
- Western Europe - junho/2015: 5789,17%
- Southern Europe - Setembro/2017: 4936,73%
Destaque para os meses de variação negativa nos lucros:¶
- Southern Europe - junho/2017: -25257,81%,
- South Asia - setembro/2016: -15387,03%
- Northern Europe - setembro/2016: -2553,40%
sns.set(rc = {'figure.figsize':(16,9)})
fig = sns.boxplot(x='Order Region', y='Benefit per order', data=df_filtrado)
plt.xticks(rotation=90);
Segmentos de clientes mais lucrativos¶
# Agrupando por 'Category Name' e somando as vendas
Lucro_por_segmento = df_filtrado.groupby('Customer Segment')['Benefit per order'].sum().reset_index()
Lucro_por_segmento['Benefit per order'] = (Lucro_por_segmento['Benefit per order'] / 1000000).round(2)
Lucro_por_segmento.rename(columns={'Benefit per order': 'Lucro (em milhões)', 'Customer Segment': 'Segmento de Cliente'}, inplace=True)
# Ordenando os resultados
Lucro_por_segmento = Lucro_por_segmento.sort_values(by='Lucro (em milhões)', ascending=False).reset_index(drop=True)
Lucro_por_segmento
| Segmento de Cliente | Lucro (em milhões) | |
|---|---|---|
| 0 | Consumer | 1.83 |
| 1 | Corporate | 1.07 |
| 2 | Home Office | 0.63 |
fig = px.bar(Lucro_por_segmento, x='Segmento de Cliente', y='Lucro (em milhões)', color = 'Segmento de Cliente',
text = 'Lucro (em milhões)', title='Lucratividade por segmento de cliente (em milhões)',
labels={'Order Region': 'Região'})
fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=500)
fig.show()
# Agora, agrupar os dados por categoria e data, somando o lucro
Lucro_por_segmento_e_data = df_filtrado.groupby(['Customer Segment', pd.Grouper(key='order date (DateOrders)', freq='M')])['Benefit per order'].sum().reset_index()
Lucro_por_segmento_e_data.rename(columns={'Customer Segment':'Segmento', 'Benefit per order': 'Lucro (em milhares)', 'order date (DateOrders)': 'Data'}, inplace=True)
# Calculando a variação dos lucros por segmento
Lucro_por_segmento_e_data['Variação de Lucro'] = Lucro_por_segmento_e_data.groupby('Segmento')['Lucro (em milhares)'].diff()
# Calculando a variação percentual dos lucros por segmento
Lucro_por_segmento_e_data['Variação Percentual'] = Lucro_por_segmento_e_data.groupby('Segmento')['Lucro (em milhares)'].pct_change()
# Convertendo a variação para porcentagem e arredondando
Lucro_por_segmento_e_data['Variação Percentual'] = (Lucro_por_segmento_e_data['Variação Percentual'] * 100).round(2)
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_segmento_e_data, x='Data', y='Lucro (em milhares)', color='Segmento', title='Lucro por segmento de cliente ao Longo do Tempo')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfic'
fig.show()
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_segmento_e_data, x='Data', y='Variação de Lucro', color='Segmento', title='Variação do lucro por segmento de cliente ao Longo do Tempo')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfic'
fig.show()
# Destacando os meses em que segmentos tiveram uma variação maior que 40% nos lucros da empresa
Lucro_por_segmento_e_data[abs(Lucro_por_segmento_e_data['Variação Percentual']) > 40.00]
| Segmento | Data | Lucro (em milhares) | Variação de Lucro | Variação Percentual | |
|---|---|---|---|---|---|
| 44 | Corporate | 2015-12-31 | 33680.339986 | 10879.279855 | 47.71 |
| 85 | Home Office | 2016-08-31 | 12522.209888 | -9716.130045 | -43.69 |
| 97 | Home Office | 2017-08-31 | 25076.379961 | 9464.320037 | 60.62 |
sns.set(rc = {'figure.figsize':(16,9)})
fig = sns.boxplot(x='Customer Segment', y='Benefit per order', data=df_filtrado)
plt.xticks(rotation=90);
Departamentos mais lucrativos¶
# Agrupando por 'Category Name' e somando as vendas
Lucro_por_depart = df_filtrado.groupby('Department Name')['Benefit per order'].sum().reset_index()
Lucro_por_depart['Benefit per order'] = (Lucro_por_depart['Benefit per order'] / 1000000).round(2)
Lucro_por_depart.rename(columns={'Benefit per order': 'Lucro (em milhões)', 'Department Name': 'Departamento'}, inplace=True)
# Ordenando os resultados
Lucro_por_depart = Lucro_por_depart.sort_values(by='Lucro (em milhões)', ascending=False).reset_index(drop=True)
Lucro_por_depart
| Departamento | Lucro (em milhões) | |
|---|---|---|
| 0 | Fan Shop | 1.76 |
| 1 | Apparel | 0.77 |
| 2 | Golf | 0.48 |
| 3 | Footwear | 0.39 |
| 4 | Outdoors | 0.11 |
| 5 | Fitness | 0.03 |
fig = px.bar(Lucro_por_depart, x='Departamento', y='Lucro (em milhões)', color = 'Departamento',
text = 'Lucro (em milhões)', title='Lucratividade por departamento (em milhões)')
fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=500)
fig.show()
# Agora, agrupar os dados por categoria e data, somando o lucro
Lucro_por_depart_e_data = df_filtrado.groupby(['Department Name', pd.Grouper(key='order date (DateOrders)', freq='M')])['Benefit per order'].sum().reset_index()
Lucro_por_depart_e_data.rename(columns={'Department Name':'Departamento', 'Benefit per order': 'Lucro (em milhares)', 'order date (DateOrders)': 'Data'}, inplace=True)
# Calculando a variação dos lucros por departamento
Lucro_por_depart_e_data['Variação de Lucro'] = Lucro_por_depart_e_data.groupby('Departamento')['Lucro (em milhares)'].diff()
# Calculando a variação percentual dos lucros por departamento
Lucro_por_depart_e_data['Variação Percentual'] = Lucro_por_depart_e_data.groupby('Departamento')['Lucro (em milhares)'].pct_change()
# Convertendo a variação para porcentagem e arredondando
Lucro_por_depart_e_data['Variação Percentual'] = (Lucro_por_depart_e_data['Variação Percentual'] * 100).round(2)
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_depart_e_data, x='Data', y='Lucro (em milhares)', color='Departamento', title='Lucro por departamento ao longo do tempo (em milhares)')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_depart_e_data, x='Data', y='Variação de Lucro', color='Departamento', title='Variação do lucro por departamento ao longo do tempo')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Destacando os meses em que departamentos tiveram uma variação maior que 300% nos lucros da empresa
Lucro_por_depart_e_data[abs(Lucro_por_depart_e_data['Variação Percentual']) > 300.00]
| Departamento | Data | Lucro (em milhares) | Variação de Lucro | Variação Percentual | |
|---|---|---|---|---|---|
| 80 | Fitness | 2016-03-31 | 841.719984 | 649.819970 | 338.62 |
| 97 | Fitness | 2017-08-31 | 1718.959974 | 1320.789978 | 331.72 |
| 113 | Footwear | 2016-03-31 | 10955.529980 | 8773.049867 | 401.98 |
Os lucros oscilaram durante todo o período.
Destaque para os meses de variação positiva nos lucros:¶
- Footwear - março/2016: 401,98%
- Fitness - março/2016: 338,62%
- Fitness - agosto/2017: 331,72%
Lucro por região do cliente¶
# Agrupando por 'Category Name' e somando as vendas
Lucro_por_regiao_do_cliente = df_filtrado.groupby('Customer Country')['Benefit per order'].sum().reset_index()
Lucro_por_regiao_do_cliente['Benefit per order'] = (Lucro_por_regiao_do_cliente['Benefit per order'] / 1000000).round(2)
Lucro_por_regiao_do_cliente.rename(columns={'Benefit per order': 'Lucro (em milhões)', 'Customer Country': 'País de origem do cliente'}, inplace=True)
# Ordenando os resultados
Lucro_por_regiao_do_cliente = Lucro_por_regiao_do_cliente.sort_values(by='Lucro (em milhões)', ascending=False).reset_index(drop=True)
Lucro_por_regiao_do_cliente
| País de origem do cliente | Lucro (em milhões) | |
|---|---|---|
| 0 | EE. UU. | 2.18 |
| 1 | Puerto Rico | 1.35 |
fig = px.bar(Lucro_por_regiao_do_cliente, x='País de origem do cliente', y='Lucro (em milhões)', color = 'País de origem do cliente',
text = 'Lucro (em milhões)', title='Lucro por região de origem do cliente')
fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=500)
fig.show()
# Agora, agrupar os dados por categoria e data, somando o lucro
Lucro_por_regiao_do_cliente_e_data = df_filtrado.groupby(['Customer Country', pd.Grouper(key='order date (DateOrders)', freq='M')])['Benefit per order'].sum().reset_index()
Lucro_por_regiao_do_cliente_e_data['Benefit per order'] = (Lucro_por_regiao_do_cliente_e_data['Benefit per order'] / 1000).round(2)
Lucro_por_regiao_do_cliente_e_data.rename(columns={'Customer Country':'País de origem do cliente', 'Benefit per order': 'Lucro (em milhares)', 'order date (DateOrders)': 'Data'}, inplace=True)
# Calculando a variação dos lucros por região do cliente
Lucro_por_regiao_do_cliente_e_data['Variação de Lucro'] = Lucro_por_regiao_do_cliente_e_data.groupby('País de origem do cliente')['Lucro (em milhares)'].diff()
# Calculando a variação percentual dos lucros por região do cliente
Lucro_por_regiao_do_cliente_e_data['Variação Percentual'] = Lucro_por_regiao_do_cliente_e_data.groupby('País de origem do cliente')['Lucro (em milhares)'].pct_change()
# Convertendo a variação para porcentagem e arredondando
Lucro_por_regiao_do_cliente_e_data['Variação Percentual'] = (Lucro_por_regiao_do_cliente_e_data['Variação Percentual'] * 100).round(2)
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_regiao_do_cliente_e_data, x='Data', y='Lucro (em milhares)', color='País de origem do cliente', title='Lucro por região do cliente - Ao Longo do Tempo')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Criar o gráfico usando Plotly
fig = px.line(Lucro_por_regiao_do_cliente_e_data, x='Data', y='Variação de Lucro', color='País de origem do cliente', title='Variação do lucro por região do cliente - Ao Longo do Tempo')
fig.update_layout(width=1000, height=500)
# Mostrar o gráfico
fig.show()
# Destacando os meses em que departamentos tiveram uma variação maior que 30% nos lucros da empresa
Lucro_por_regiao_do_cliente_e_data[abs(Lucro_por_regiao_do_cliente_e_data['Variação Percentual']) > 30.00]
| País de origem do cliente | Data | Lucro (em milhares) | Variação de Lucro | Variação Percentual | |
|---|---|---|---|---|---|
| 45 | Puerto Rico | 2016-01-31 | 30.64 | -15.26 | -33.25 |
| 47 | Puerto Rico | 2016-03-31 | 39.55 | 11.04 | 38.72 |
7.1 Risco de atraso nas entregas¶
risco_entrega_atrasada = df_filtrado['Late_delivery_risk'].apply(lambda lat: 'Risco Alto' if lat == 1 else 'Risco Baixo')
risco_entrega_atrasada_count = risco_entrega_atrasada.value_counts()
# Tamanho do gráfico
plt.figure(figsize=(8,6))
# Cria um gráfico de barras com índice e contagem
barra = plt.bar(
risco_entrega_atrasada_count.index, # valor no eixo x
risco_entrega_atrasada_count.values, # valor no eixo y
color = ['steelblue', 'lightcoral'] # cores das barras
)
# Rotulo do eixo y, letra tamanho 8
plt.ylabel('Número de entregas', fontsize = 12)
# Titulo, letra tamanho 14
plt.title('Risco de atraso nas entregas da empresa', fontsize = 16)
# Adicionando a contagem em cima das barras
for bar in barra:
yval = bar.get_height()
plt.text(bar.get_x() + bar.get_width()/2.0, yval, int(yval), va='bottom', ha='center', fontsize=13)
plt.show()
risco_entrega_atrasada.value_counts(1).round(4)
Late_delivery_risk Risco Alto 0.5728 Risco Baixo 0.4272 Name: proportion, dtype: float64
Pela análise foi possível verificar que, no período entre janeiro/2015 à setembro/2017, 57.28% das entregas realizadas pela empresa tiveram um alto risco de atraso na entrega.
Risco de atraso por departamento¶
risco_atraso_depart = df_filtrado.groupby(['Department Name', 'Late_delivery_risk']).size().unstack(fill_value=0)
# Renomeando colunas
risco_atraso_depart.rename(columns={'Department Name':'Departamento', 0: 'Risco Baixo', 1: 'Risco Alto'}, inplace=True)
risco_atraso_depart = risco_atraso_depart.reset_index()
# Removendo a coluna Late_delivery_risk
risco_atraso_depart.columns = ['Departamento', 'Risco Baixo', 'Risco Alto']
risco_atraso_depart
| Departamento | Risco Baixo | Risco Alto | |
|---|---|---|---|
| 0 | Apparel | 19122 | 25588 |
| 1 | Fan Shop | 27155 | 36291 |
| 2 | Fitness | 856 | 1174 |
| 3 | Footwear | 5923 | 7934 |
| 4 | Golf | 13553 | 18179 |
| 5 | Outdoors | 3695 | 5102 |
fig = go.Figure()
# Risco de entrega baixo (Late_delivery_risk = 0)
fig.add_trace(go.Bar(x=risco_atraso_depart['Departamento'], y=risco_atraso_depart['Risco Baixo'], name='Risco Baixo', marker_color='blue'))
# Risco de entrega alto (Late_delivery_risk = 1)
fig.add_trace(go.Bar(x=risco_atraso_depart['Departamento'], y=risco_atraso_depart['Risco Alto'], name='Risco Alto', marker_color='red'))
# Atualizando o layout do gráfico
fig.update_layout(title='Risco de atraso em entrega por departamento', xaxis=dict(title='Nome do Departamento'),
yaxis=dict(title='Contagem'), barmode='group')
fig.update_layout(width=1000, height=500)
# Mostrando o gráfico
fig.show()
Pelo gráfico acima pode-se concluir que para todos os departamentos o risco da entregas atrasar é majoritariamente alto.
Risco de atraso por mercado global¶
risco_atraso_mercado_global = df_filtrado.groupby(['Market', 'Late_delivery_risk']).size().unstack(fill_value=0)
# Renomeando colunas
risco_atraso_mercado_global.rename(columns={0: 'Risco Baixo', 1: 'Risco Alto'}, inplace=True)
risco_atraso_mercado_global = risco_atraso_mercado_global.reset_index()
# Removendo a coluna Late_delivery_risk
risco_atraso_mercado_global.columns = ['Mercado Global', 'Risco Baixo', 'Risco Alto']
risco_atraso_mercado_global
| Mercado Global | Risco Baixo | Risco Alto | |
|---|---|---|---|
| 0 | Africa | 4814 | 6340 |
| 1 | Europe | 19124 | 26153 |
| 2 | LATAM | 21265 | 28044 |
| 3 | Pacific Asia | 14612 | 19593 |
| 4 | USCA | 10489 | 14138 |
fig = go.Figure()
# Risco de entrega baixo (Late_delivery_risk = 0)
fig.add_trace(go.Bar(x=risco_atraso_mercado_global['Mercado Global'], y=risco_atraso_mercado_global['Risco Baixo'], name='Risco Baixo', marker_color='blue'))
# Risco de entrega alto (Late_delivery_risk = 1)
fig.add_trace(go.Bar(x=risco_atraso_mercado_global['Mercado Global'], y=risco_atraso_mercado_global['Risco Alto'], name='Risco Alto', marker_color='red'))
# Atualizando o layout do gráfico
fig.update_layout(title='Risco de atraso em entrega por Mercado Global', xaxis=dict(title='Mercado Global'),
yaxis=dict(title='Contagem'), barmode='group')
fig.update_layout(width=1000, height=500)
# Mostrando o gráfico
fig.show()
Ou seja, em cada um dos mercados globais a maioria dos pedidos tem um alto risco da entrega atrasar.
Proporção de risco de atraso alto por categoria¶
risco_atraso_cat = df_filtrado.groupby(['Category Name', 'Late_delivery_risk']).size().unstack(fill_value=0)
# Renomeando colunas
risco_atraso_cat.rename(columns={0: 'Risco Baixo', 1: 'Risco Alto'}, inplace=True)
risco_atraso_cat = risco_atraso_cat.reset_index()
# Removendo a coluna Late_delivery_risk
risco_atraso_cat.columns = ['Category Name', 'Risco Baixo', 'Risco Alto']
# Criando uma coluna chamada 'Proporção de Alto Risco de Atraso'
risco_atraso_cat['Proporção de Alto Risco de Atraso'] = (risco_atraso_cat['Risco Alto']/(risco_atraso_cat['Risco Alto']+risco_atraso_cat['Risco Baixo'])).round(3)
# Ordenando o DataFrame pelo 'Proporção de Alto Risco de Atraso'
risco_atraso_cat.sort_values(by='Proporção de Alto Risco de Atraso', ascending=False, inplace=True)
risco_atraso_cat = risco_atraso_cat.reset_index(drop=True)
risco_atraso_cat
| Category Name | Risco Baixo | Risco Alto | Proporção de Alto Risco de Atraso | |
|---|---|---|---|---|
| 0 | Golf Bags & Carts | 19 | 42 | 0.689 |
| 1 | Lacrosse | 123 | 206 | 0.626 |
| 2 | Strength Training | 40 | 60 | 0.600 |
| 3 | Accessories | 683 | 1014 | 0.598 |
| 4 | Fitness Accessories | 118 | 175 | 0.597 |
| 5 | Boxing & MMA | 164 | 238 | 0.592 |
| 6 | As Seen on TV! | 27 | 39 | 0.591 |
| 7 | Trade-In | 384 | 542 | 0.585 |
| 8 | Electronics | 1254 | 1770 | 0.585 |
| 9 | Golf Gloves | 427 | 602 | 0.585 |
| 10 | Girls' Apparel | 473 | 664 | 0.584 |
| 11 | Tennis & Racquet | 131 | 183 | 0.583 |
| 12 | Hunting & Shooting | 177 | 247 | 0.583 |
| 13 | Golf Shoes | 211 | 291 | 0.580 |
| 14 | Shop By Sport | 4453 | 6053 | 0.576 |
| 15 | Baseball & Softball | 258 | 349 | 0.575 |
| 16 | Golf Balls | 598 | 805 | 0.574 |
| 17 | Cleats | 9996 | 13481 | 0.574 |
| 18 | Kids' Golf Clubs | 150 | 202 | 0.574 |
| 19 | Fishing | 7072 | 9508 | 0.573 |
| 20 | Water Sports | 6347 | 8508 | 0.573 |
| 21 | Indoor/Outdoor Games | 7897 | 10548 | 0.572 |
| 22 | Women's Apparel | 8627 | 11462 | 0.571 |
| 23 | Basketball | 24 | 32 | 0.571 |
| 24 | Women's Golf Clubs | 73 | 97 | 0.571 |
| 25 | Men's Footwear | 9126 | 12107 | 0.570 |
| 26 | Cardio Equipment | 5131 | 6795 | 0.570 |
| 27 | Camping & Hiking | 5662 | 7480 | 0.569 |
| 28 | Hockey | 259 | 329 | 0.560 |
| 29 | Soccer | 61 | 75 | 0.551 |
| 30 | Golf Apparel | 200 | 229 | 0.534 |
| 31 | Men's Golf Clubs | 139 | 135 | 0.493 |
Pelo dataframe acima pode-se concluir que, com exceção de uma categoria (Men's Golf Clubs), em todos as outras os pedidos realizados têm alto risco de atraso na entrega em sua maioria.
7.2 Entrega real VS Entrega agendada¶
df_filtrado['Days for shipping (real)'].describe()
count 164572.000000 mean 3.498232 std 1.623669 min 0.000000 25% 2.000000 50% 3.000000 75% 5.000000 max 6.000000 Name: Days for shipping (real), dtype: float64
df_filtrado['Days for shipping (real)'].value_counts()
Days for shipping (real) 2 51648 3 26187 6 26186 4 25985 5 25715 0 4628 1 4223 Name: count, dtype: int64
df_filtrado['Days for shipment (scheduled)'].describe()
count 164572.000000 mean 2.933051 std 1.373475 min 0.000000 25% 2.000000 50% 4.000000 75% 4.000000 max 4.000000 Name: Days for shipment (scheduled), dtype: float64
# Define a localidade para adicionar o separador de milhares
locale.setlocale(locale.LC_ALL, '')
# Cria uma figura com 2 subplots
fig, axs = plt.subplots(1, 2, figsize=(16, 9))
# Cria um boxplot para a variável 'entrega real' no primeiro subplot
bp1 = axs[0].boxplot(df_filtrado['Days for shipping (real)'], patch_artist=True)
axs[0].set_title('Boxplot dos dias reais de entrega')
# Calcula a média e mediana dias de entrega real
mean_entrega_real = np.mean(df_filtrado['Days for shipping (real)'])
median_entrega_real = np.median(df_filtrado['Days for shipping (real)'])
# Define a cor do boxplot
bp1['boxes'][0].set_facecolor('lightblue')
# Adiciona a legenda da média e mediana com separador de milhares
max_rent = np.max(df_filtrado['Days for shipping (real)'])
axs[0].annotate(f'Média = {locale.format_string("%.2f", mean_entrega_real, grouping=True)}\nMediana = {locale.format_string("%.2f", median_entrega_real, grouping=True)}',
xy=(1, max_rent*0.8),
xytext=(1.15, max_rent*0.8),
bbox=dict(facecolor='lightblue', edgecolor='blue'),
fontsize=10)
# Cria um boxplot para a variável 'entrega agendada' no segundo subplot
bp2 = axs[1].boxplot(df_filtrado['Days for shipment (scheduled)'], patch_artist=True)
axs[1].set_title('Boxplot dos dias previstos de entrega')
# Calcula a média e mediana dos dias previstos para entrega
mean_entrega_agendada = np.mean(df_filtrado['Days for shipment (scheduled)'])
median_entrega_agendada = np.median(df_filtrado['Days for shipment (scheduled)'])
# Define a cor do boxplot
bp2['boxes'][0].set_facecolor('lightgreen')
# Adiciona a legenda da média e mediana com separador de milhares
max_total = np.max(df_filtrado['Days for shipment (scheduled)'])
axs[1].annotate(f'Média = {locale.format_string("%.2f", mean_entrega_agendada, grouping=True)}\nMediana = {locale.format_string("%.2f", median_entrega_agendada, grouping=True)}',
xy=(1, max_total*0.8),
xytext=(1.15, max_total*0.8),
bbox=dict(facecolor='lightgreen', edgecolor='green'),
fontsize=10)
# Mostra os gráficos
plt.show()
Pela análise dos boxplots:
Distribuição dos dias reais de entrega
- O conjunto de dados dos dias reais de entrega não possui outliers superiores ou inferiores
- O boxplot mostra que a mediana (Q2) está mais próxima de Q1 do que de Q3, mostrando que existe uma cauda mais longa com números maiores, possuindo mais registros com valores baixos do que altos, trata-se de uma distribuição assimétrica à direita. Isso indica que a maioria dos produtos demoram poucos dias para serem entregues (não significando que não estão em atraso).
Distribuição dos dias previstos de entrega
- O conjunto de dados dos dias previstos de entrega não possui outliers superiores ou inferiores
- O boxplot mostra que a mediana (Q2) é exatamente igual ao Q3 e também ao valor máximo, indicando que na grande maioria dos casos a empresa acaba prevendo uma mesma quantidade de dias para a entrega de produtos. Isso ainda contribui para a formação de uma cauda mais longa com números menores, possuindo mais registros com valores altos do que baixos, caracterizando uma distribuição assimétrica à esquerda
mediana_entrega_real = df_filtrado['Days for shipping (real)'].median()
mediana_entrega_agendada = df_filtrado['Days for shipment (scheduled)'].median()
mediana_entrega_real_format = (
"{:,.0f}".format(mediana_entrega_real)
.replace(",", ".")
)
mediana_entrega_agendada_format = (
"{:,.0f}".format(mediana_entrega_agendada)
.replace(",", ".")
)
# Crie dois subplots (um para o histograma de 'entrega real' e outro para o histograma de 'entrega agendada')
fig = make_subplots(rows=1, cols=2, subplot_titles=('Histograma dos dias para entrega - real', 'Histograma dos dias para entrega - previsto'))
# Adicione o histograma de 'entrega real' ao primeiro subplot
histogram_entrega_real = go.Histogram(x=df_filtrado['Days for shipping (real)'], nbinsx=8, marker=dict(color='blue'))
fig.add_trace(histogram_entrega_real, row=1, col=1)
# Adicione o histograma de 'entrega agendada' ao segundo subplot
histogram_entrega_agendada = go.Histogram(x=df_filtrado['Days for shipment (scheduled)'], nbinsx=8, marker=dict(color='green'))
fig.add_trace(histogram_entrega_agendada, row=1, col=2)
# Adicione a linha da mediana a ambos os subplots
line_entrega_real = go.Scatter(x=[mediana_entrega_real, mediana_entrega_real], y=[0, 60000], mode='lines',
line=dict(color='lightblue', dash='dash'),
showlegend=True,
name=f"Mediana dos dias para entrega - real = {mediana_entrega_real_format}")
fig.add_trace(line_entrega_real, row=1, col=1)
line_entrega_agendada = go.Scatter(x=[mediana_entrega_agendada, mediana_entrega_agendada], y=[0, 120000], mode='lines',
line=dict(color='lightgreen', dash='dash'),
showlegend=True,
name=f"Mediana dos dias para entrega - agendada = {mediana_entrega_agendada_format}")
fig.add_trace(line_entrega_agendada, row=1, col=2)
# Atualize o layout e as configurações dos subplots
fig.update_layout(title_text='Histograma da quantidade de dias para entrega real e agendada',
autosize=False,
width=1200, # Largura total dos subplots
height=500)
fig.update_yaxes(range=[0, 60000], row=1, col=1)
fig.update_yaxes(range=[0, 120000], row=1, col=2)
# Mostre o gráfico
fig.show()
Os histogramas indicados acima acabam comprovando o comportamento das distribuições verificado pelos boxplots anteriormente.
fig, axes = plt.subplots(1, 2, figsize = (18,9))
plt.tight_layout()
sns.boxplot(y=df_filtrado['Days for shipping (real)'] - df_filtrado['Days for shipment (scheduled)'],ax= axes[0], palette = 'YlGnBu_r', orient='v')
axes[0].set_title('Boxplot - Delay entre dias reais e previstos para entrega', fontsize=18)
sns.histplot(df_filtrado['Days for shipping (real)'] - df_filtrado['Days for shipment (scheduled)'], ax=axes[1], color='lightblue', bins=8)
axes[1].set_title('Histograma - Delay entre dias reais e previstos para entrega', fontsize=18)
plt.show()
delay = df_filtrado['Days for shipping (real)'] - df_filtrado['Days for shipment (scheduled)']
delay.describe()
count 164572.000000 mean 0.565181 std 1.492961 min -2.000000 25% 0.000000 50% 1.000000 75% 1.000000 max 4.000000 dtype: float64
O boxplot e o histograma indicam que 75% dos pedidos possuem até 1 dia de atraso, sendo 4 dias o máximo de atraso que um pedido teve e 2 dias o máximo de adiantamento na chegada.
print(f"A assimetria da distribuição do delay assume valor: {skew(delay):.2f}, ou seja, é levemente assimétrica à esquerda.")
A assimetria da distribuição do delay assume valor: 0.03, ou seja, é levemente assimétrica à esquerda.
delay.value_counts(1)
1 0.335452 0 0.186417 2 0.159493 -2 0.120786 -1 0.119990 3 0.039144 4 0.038719 Name: proportion, dtype: float64
# Contando o número de pedidos com atraso de pelo menos 1 dia
delay_1_dia = (delay >= 1).sum()
# Calculando o número total de pedidos
total_de_pedidos = len(delay)
# Calculando a porcentagem de pedidos com atraso
porcentagem_com_atraso = (delay_1_dia / total_de_pedidos) * 100
# Exibindo o resultado
print(f"Porcentagem de pedidos com atraso de pelo menos 1 dia: {porcentagem_com_atraso:.2f}%")
print()
print(f"Porcentagem de pedidos com apenas 1 dia de atraso: {delay.value_counts(1)[1]:.2f}%")
print()
print(f"Porcentagem de pedidos com 2 dias de atraso: {delay.value_counts(1)[2]:.2f}%")
print()
print(f"Porcentagem de pedidos com 3 dias de atraso: {delay.value_counts(1)[3]:.2f}%")
print()
print(f"Porcentagem de pedidos com 4 dias de atraso: {delay.value_counts(1)[4]:.2f}%")
Porcentagem de pedidos com atraso de pelo menos 1 dia: 57.28% Porcentagem de pedidos com apenas 1 dia de atraso: 0.34% Porcentagem de pedidos com 2 dias de atraso: 0.16% Porcentagem de pedidos com 3 dias de atraso: 0.04% Porcentagem de pedidos com 4 dias de atraso: 0.04%
print(f"Porcentagem de pedidos com um dia adiantado: {delay.value_counts(1)[-1]:.2f}%")
print()
print(f"Porcentagem de pedidos com dois dias adiantado: {delay.value_counts(1)[-2]:.2f}%")
Porcentagem de pedidos com um dia adiantado: 0.12% Porcentagem de pedidos com dois dias adiantado: 0.12%
print(f"Porcentagem de pedidos que chegou exatamente no dia previsto: {delay.value_counts(1)[0]:.2f}%")
Porcentagem de pedidos que chegou exatamente no dia previsto: 0.19%
# Obter a contagem para cada status de entrega
delay = (df_filtrado['Days for shipping (real)'] - df_filtrado['Days for shipment (scheduled)']).value_counts()
delay_df = delay.reset_index()
delay_df.columns = ['Dias de Atraso', 'Quantidade de Pedidos']
# Convertendo 'Dias de Atraso' para string para obter cores distintas
delay_df['Dias de Atraso'] = delay_df['Dias de Atraso'].astype(str)
fig = px.bar(delay_df, y='Dias de Atraso', x='Quantidade de Pedidos', color='Dias de Atraso',
title="Quantidade de Pedidos de acordo com os dias de atraso", text = 'Quantidade de Pedidos')
fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=500)
fig.show()
7.3 Análise dos status de entrega¶
# Obter a contagem para cada status de entrega
status_entrega = df_filtrado['Delivery Status'].value_counts()
status_entrega_df = status_entrega.reset_index()
status_entrega_df.columns = ['Status de Entrega', 'Quantidade de Pedidos']
fig = px.bar(status_entrega_df, x='Status de Entrega', y='Quantidade de Pedidos', color='Status de Entrega',
title="Quantidade de Pedidos por Status de Entrega", text = 'Quantidade de Pedidos')
fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=500)
fig.show()
status_entrega_tempo = df_filtrado.groupby([df['order date (DateOrders)'].dt.to_period('M'), 'Delivery Status']).size()
status_entrega_tempo = status_entrega_tempo.unstack(fill_value=0)
status_entrega_tempo.index = status_entrega_tempo.index.to_timestamp()
fig = px.line(status_entrega_tempo, title='Status de Entrega ao Longo do Tempo',
labels={'value': 'Quantidade de Pedidos', 'order date (DateOrders)': 'Data', 'Delivery Status': 'Status de Entrega'})
fig.update_layout(width=1000, height=500)
fig.show()
atraso_cat = df_filtrado.groupby(['Category Name', 'Delivery Status']).size().unstack(fill_value=0)
atraso_cat = atraso_cat.reset_index()
# Removendo a coluna Delivery Status
#atraso_cat.columns = ['Category Name', 'Advance shipping', 'Late delivery', 'Shipping canceled', 'Shipping on time']
atraso_cat.columns = ['Category Name', 'Advance shipping', 'Late delivery', 'Shipping on time']
# Criando uma coluna chamada 'Proporção de entregas em atraso'
atraso_cat['Proporção de entregas em atraso'] = (atraso_cat['Late delivery']/(atraso_cat['Late delivery']+atraso_cat['Advance shipping']+atraso_cat['Shipping on time'])).round(3)
# Ordenando o DataFrame pelo 'Proporção de entregas em atraso'
atraso_cat.sort_values(by='Proporção de entregas em atraso', ascending=False, inplace=True)
atraso_cat = atraso_cat.reset_index(drop=True)
atraso_cat
| Category Name | Advance shipping | Late delivery | Shipping on time | Proporção de entregas em atraso | |
|---|---|---|---|---|---|
| 0 | Golf Bags & Carts | 12 | 42 | 7 | 0.689 |
| 1 | Lacrosse | 75 | 206 | 48 | 0.626 |
| 2 | Strength Training | 20 | 60 | 20 | 0.600 |
| 3 | Accessories | 406 | 1014 | 277 | 0.598 |
| 4 | Fitness Accessories | 69 | 175 | 49 | 0.597 |
| 5 | Boxing & MMA | 84 | 238 | 80 | 0.592 |
| 6 | As Seen on TV! | 15 | 39 | 12 | 0.591 |
| 7 | Trade-In | 232 | 542 | 152 | 0.585 |
| 8 | Electronics | 733 | 1770 | 521 | 0.585 |
| 9 | Golf Gloves | 240 | 602 | 187 | 0.585 |
| 10 | Girls' Apparel | 284 | 664 | 189 | 0.584 |
| 11 | Tennis & Racquet | 65 | 183 | 66 | 0.583 |
| 12 | Hunting & Shooting | 101 | 247 | 76 | 0.583 |
| 13 | Golf Shoes | 120 | 291 | 91 | 0.580 |
| 14 | Shop By Sport | 2534 | 6053 | 1919 | 0.576 |
| 15 | Baseball & Softball | 141 | 349 | 117 | 0.575 |
| 16 | Golf Balls | 357 | 805 | 241 | 0.574 |
| 17 | Cleats | 5559 | 13481 | 4437 | 0.574 |
| 18 | Kids' Golf Clubs | 83 | 202 | 67 | 0.574 |
| 19 | Fishing | 4031 | 9508 | 3041 | 0.573 |
| 20 | Water Sports | 3539 | 8508 | 2808 | 0.573 |
| 21 | Indoor/Outdoor Games | 4444 | 10548 | 3453 | 0.572 |
| 22 | Women's Apparel | 4877 | 11462 | 3750 | 0.571 |
| 23 | Basketball | 20 | 32 | 4 | 0.571 |
| 24 | Women's Golf Clubs | 47 | 97 | 26 | 0.571 |
| 25 | Men's Footwear | 5095 | 12107 | 4031 | 0.570 |
| 26 | Cardio Equipment | 2915 | 6795 | 2216 | 0.570 |
| 27 | Camping & Hiking | 3142 | 7480 | 2520 | 0.569 |
| 28 | Hockey | 158 | 329 | 101 | 0.560 |
| 29 | Soccer | 28 | 75 | 33 | 0.551 |
| 30 | Golf Apparel | 116 | 229 | 84 | 0.534 |
| 31 | Men's Golf Clubs | 83 | 135 | 56 | 0.493 |
Pelo dataframe acima pode-se concluir que, com exceção de uma categoria (Men's Golf Clubs), em todos as outras a entregas em atraso superam a soma das entregas no prazo, canceladas e antes do prazo.
7.4 Análise dos status de pedido¶
# Obter a contagem para cada status de entrega
status_pedido = df_filtrado['Order Status'].value_counts()
status_pedido_df = status_pedido.reset_index()
status_pedido_df.columns = ['Status de Pedido', 'Quantidade de Pedidos']
fig = px.bar(status_pedido_df, x='Status de Pedido', y='Quantidade de Pedidos', color='Status de Pedido',
title="Quantidade de pedidos por status de pedido", text = 'Quantidade de Pedidos')
fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=500)
fig.show()
status_pedido_tempo = df_filtrado.groupby([df['order date (DateOrders)'].dt.to_period('M'), 'Order Status']).size()
status_pedido_tempo = status_pedido_tempo.unstack(fill_value=0)
status_pedido_tempo.index = status_pedido_tempo.index.to_timestamp()
fig = px.line(status_pedido_tempo, title='Status de Pedido ao Longo do Tempo',
labels={'value': 'Quantidade de Pedidos', 'order date (DateOrders)': 'Data', 'Order Status': 'Status de Pedido'})
fig.update_layout(width=1000, height=500)
fig.show()
7.5 Análise de pedidos com entrega cancelada¶
entrega_cancelada_df = df[df['Delivery Status']=='Shipping canceled']
entrega_cancelada_df = entrega_cancelada_df['Order Status'].value_counts()
entrega_cancelada_df = entrega_cancelada_df.reset_index()
entrega_cancelada_df.columns = ['Motivo do Cancelamento', 'Quantidade de Pedidos']
entrega_cancelada_df['Motivo do Cancelamento'] = entrega_cancelada_df['Motivo do Cancelamento'].replace({'SUSPECTED_FRAUD': 'POSSIVEL FRAUDE', 'CANCELED': 'CANCELADO PELO USUARIO'})
entrega_cancelada_df
| Motivo do Cancelamento | Quantidade de Pedidos | |
|---|---|---|
| 0 | POSSIVEL FRAUDE | 3877 |
| 1 | CANCELADO PELO USUARIO | 3513 |
fig = px.bar(entrega_cancelada_df, x='Motivo do Cancelamento', y='Quantidade de Pedidos', color='Motivo do Cancelamento',
title="Pedidos Com Entrega Cancelada", text = 'Quantidade de Pedidos')
fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()
Percebe-se que, dos pedidos que tiveram entrega cancelada, ou foram cancelados pelo próprio cliente ou por suspeita de fraude a própria empresa cancelou.
operacoes_fraude = df[df['Order Status']=='SUSPECTED_FRAUD']
operacoes_fraude.head()
| Type | Days for shipping (real) | Days for shipment (scheduled) | Benefit per order | Sales per customer | Delivery Status | Late_delivery_risk | Category Id | Category Name | Customer City | Customer Country | Customer Email | Customer Fname | Customer Id | Customer Lname | Customer Password | Customer Segment | Customer State | Customer Street | Customer Zipcode | Department Id | Department Name | Latitude | Longitude | Market | Order City | Order Country | Order Customer Id | order date (DateOrders) | Order Id | Order Item Cardprod Id | Order Item Discount | Order Item Discount Rate | Order Item Id | Order Item Product Price | Order Item Profit Ratio | Order Item Quantity | Sales | Order Item Total | Order Profit Per Order | Order Region | Order State | Order Status | Order Zipcode | Product Card Id | Product Category Id | Product Description | Product Image | Product Name | Product Price | Product Status | shipping date (DateOrders) | Shipping Mode | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 183 | TRANSFER | 5 | 4 | 28.850000 | 128.220001 | Shipping canceled | 0 | 13 | Electronics | Freeport | EE. UU. | XXXXXXXXX | Patricia | 1509 | Petersen | XXXXXXXXX | Consumer | NY | 3675 Emerald Goose Bank | 11520.0 | 3 | Footwear | 40.654865 | -73.587074 | USCA | Houston | Estados Unidos | 1509 | 2016-05-08 17:42:00 | 33824 | 278 | 6.75 | 0.05 | 84424 | 44.990002 | 0.23 | 3 | 134.970001 | 128.220001 | 28.850000 | US Center | Texas | SUSPECTED_FRAUD | 77041.0 | 278 | 13 | NaN | http://images.acmesports.sports/Under+Armour+Men%27s+Compression+EV+SL+Slide | Under Armour Men's Compression EV SL Slide | 44.990002 | 0 | 5/13/2016 17:42 | Standard Class |
| 184 | TRANSFER | 5 | 4 | 133.910004 | 278.970001 | Shipping canceled | 0 | 9 | Cardio Equipment | Fort Washington | EE. UU. | XXXXXXXXX | Julie | 1636 | Petersen | XXXXXXXXX | Consumer | MD | 9375 Harvest Circuit | 20744.0 | 3 | Footwear | 38.742664 | -76.991798 | USCA | Gilbert | Estados Unidos | 1636 | 2016-04-02 19:51:00 | 31364 | 191 | 21.00 | 0.07 | 78398 | 99.989998 | 0.48 | 3 | 299.970001 | 278.970001 | 133.910004 | West of USA | Arizona | SUSPECTED_FRAUD | 85234.0 | 191 | 9 | NaN | http://images.acmesports.sports/Nike+Men%27s+Free+5.0%2B+Running+Shoe | Nike Men's Free 5.0+ Running Shoe | 99.989998 | 0 | 4/7/2016 19:51 | Standard Class |
| 185 | TRANSFER | 6 | 4 | 79.160004 | 272.970001 | Shipping canceled | 0 | 9 | Cardio Equipment | Bakersfield | EE. UU. | XXXXXXXXX | Lisa | 2784 | Smith | XXXXXXXXX | Consumer | CA | 3993 Thunder Hills Port | 93304.0 | 3 | Footwear | 35.362545 | -119.018700 | USCA | San Jose | Estados Unidos | 2784 | 2016-08-14 02:51:00 | 40495 | 191 | 27.00 | 0.09 | 101052 | 99.989998 | 0.29 | 3 | 299.970001 | 272.970001 | 79.160004 | West of USA | California | SUSPECTED_FRAUD | 95123.0 | 191 | 9 | NaN | http://images.acmesports.sports/Nike+Men%27s+Free+5.0%2B+Running+Shoe | Nike Men's Free 5.0+ Running Shoe | 99.989998 | 0 | 8/20/2016 2:51 | Standard Class |
| 186 | TRANSFER | 5 | 4 | 19.110001 | 272.970001 | Shipping canceled | 0 | 9 | Cardio Equipment | Fort Washington | EE. UU. | XXXXXXXXX | Julie | 1636 | Petersen | XXXXXXXXX | Consumer | MD | 9375 Harvest Circuit | 20744.0 | 3 | Footwear | 38.742664 | -76.991798 | USCA | Gilbert | Estados Unidos | 1636 | 2016-04-02 19:51:00 | 31364 | 191 | 27.00 | 0.09 | 78396 | 99.989998 | 0.07 | 3 | 299.970001 | 272.970001 | 19.110001 | West of USA | Arizona | SUSPECTED_FRAUD | 85234.0 | 191 | 9 | NaN | http://images.acmesports.sports/Nike+Men%27s+Free+5.0%2B+Running+Shoe | Nike Men's Free 5.0+ Running Shoe | 99.989998 | 0 | 4/7/2016 19:51 | Standard Class |
| 187 | TRANSFER | 2 | 4 | 10.770000 | 170.970001 | Shipping canceled | 0 | 17 | Cleats | Corona | EE. UU. | XXXXXXXXX | Tyler | 9174 | Smith | XXXXXXXXX | Consumer | NY | 9414 Thunder Island Court | 11368.0 | 4 | Apparel | 40.742107 | -73.869621 | USCA | Fresno | Estados Unidos | 9174 | 2016-05-18 16:38:00 | 34506 | 365 | 9.00 | 0.05 | 86191 | 59.990002 | 0.06 | 3 | 179.970001 | 170.970001 | 10.770000 | West of USA | California | SUSPECTED_FRAUD | 93727.0 | 365 | 17 | NaN | http://images.acmesports.sports/Perfect+Fitness+Perfect+Rip+Deck | Perfect Fitness Perfect Rip Deck | 59.990002 | 0 | 5/20/2016 16:38 | Standard Class |
7.6 Análise de fraudes em operações¶
Fraudes em produtos¶
fraude_prod = operacoes_fraude.groupby('Product Name').size().reset_index(name='Compras Fraudulentas')
fraude_prod.sort_values(by='Compras Fraudulentas', ascending=False, inplace=True)
# Renomeando colunas
fraude_prod.rename(columns={'Product Name': 'Nome do Produto'}, inplace=True)
fraude_prod
| Nome do Produto | Compras Fraudulentas | |
|---|---|---|
| 51 | Perfect Fitness Perfect Rip Deck | 560 |
| 39 | Nike Men's CJ Elite 2 TD Football Cleat | 516 |
| 42 | Nike Men's Dri-FIT Victory Golf Polo | 481 |
| 49 | O'Brien Men's Neoprene Life Vest | 439 |
| 16 | Field & Stream Sportsman 16 Gun Fire Safe | 393 |
| ... | ... | ... |
| 19 | Garmin Forerunner 910XT GPS Watch | 1 |
| 15 | Elevation Training Mask 2.0 | 1 |
| 11 | Diamondback Boys' Insight 24 Performance Hybr | 1 |
| 5 | Brooks Women's Ghost 6 Running Shoe | 1 |
| 86 | insta-bed Neverflat Air Mattress | 1 |
87 rows × 2 columns
fig = px.bar(fraude_prod.head(10), x='Nome do Produto', y='Compras Fraudulentas', color='Nome do Produto',
title="Top 10 produtos com mais compras fraudulentas", text = 'Compras Fraudulentas')
fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()
Fraudes em categorias¶
fraude_cat = operacoes_fraude.groupby('Category Name').size().reset_index(name='Compras Fraudulentas')
fraude_cat.sort_values(by='Compras Fraudulentas', ascending=False, inplace=True)
# Renomeando colunas
fraude_cat.rename(columns={'Category Name': 'Nome da Categoria'}, inplace=True)
fraude_cat
| Nome da Categoria | Compras Fraudulentas | |
|---|---|---|
| 6 | Cleats | 560 |
| 20 | Men's Footwear | 516 |
| 28 | Women's Apparel | 481 |
| 17 | Indoor/Outdoor Games | 439 |
| 8 | Fishing | 393 |
| 27 | Water Sports | 329 |
| 4 | Camping & Hiking | 305 |
| 5 | Cardio Equipment | 276 |
| 22 | Shop By Sport | 228 |
| 7 | Electronics | 77 |
| 0 | Accessories | 44 |
| 10 | Girls' Apparel | 35 |
| 12 | Golf Balls | 33 |
| 26 | Trade-In | 27 |
| 13 | Golf Gloves | 22 |
| 18 | Kids' Golf Clubs | 13 |
| 15 | Hockey | 12 |
| 3 | Boxing & MMA | 12 |
| 1 | Baseball & Softball | 11 |
| 14 | Golf Shoes | 10 |
| 16 | Hunting & Shooting | 9 |
| 19 | Lacrosse | 8 |
| 29 | Women's Golf Clubs | 8 |
| 11 | Golf Apparel | 6 |
| 25 | Tennis & Racquet | 6 |
| 9 | Fitness Accessories | 6 |
| 21 | Men's Golf Clubs | 5 |
| 2 | Basketball | 3 |
| 23 | Soccer | 2 |
| 24 | Strength Training | 1 |
fig = px.bar(fraude_cat.head(10), x='Nome da Categoria', y='Compras Fraudulentas', color='Nome da Categoria',
title="Top 10 categorias com mais compras fraudulentas", text = 'Compras Fraudulentas')
fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()
Fraudes em departamentos¶
fraude_dep = operacoes_fraude.groupby('Department Name').size().reset_index(name='Compras Fraudulentas')
fraude_dep.sort_values(by='Compras Fraudulentas', ascending=False, inplace=True)
# Renomeando colunas
fraude_dep.rename(columns={'Department Name': 'Departamento'}, inplace=True)
fraude_dep
| Departamento | Compras Fraudulentas | |
|---|---|---|
| 1 | Fan Shop | 1475 |
| 0 | Apparel | 1076 |
| 4 | Golf | 744 |
| 3 | Footwear | 328 |
| 5 | Outdoors | 212 |
| 2 | Fitness | 42 |
fig = px.bar(fraude_dep.head(10), x='Departamento', y='Compras Fraudulentas', color='Departamento',
title="Compras fraudulentas por departamento", text = 'Compras Fraudulentas')
fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=500)
fig.show()
Fraudes por hora¶
# Criando a coluna target
df['target'] = df['Order Status'].apply(lambda x: 1 if x == 'SUSPECTED_FRAUD' else 0)
# Agrupando por hora e status de fraude e contando as ocorrências
fraude_por_hora = df.groupby([df['order date (DateOrders)'].dt.hour.rename('Hora'), 'target']).size().reset_index(name='Contagem')
# Agora, usando o Seaborn para criar um gráfico de barras
plt.figure(figsize=(14, 8))
barplot = sns.barplot(data=fraude_por_hora, x='Hora', y='Contagem', hue='target')
# Adicionando títulos e rótulos
plt.title('Contagem de transações fraudulentas e não fraudulentas por hora do dia')
plt.xlabel('Hora do Dia')
plt.ylabel('Número de Transações')
# Ajustando os rótulos da legenda
handles, labels = barplot.get_legend_handles_labels()
barplot.legend(handles=handles, title='Status da Transação', labels=['Não Fraude', 'Fraude'])
plt.show()
Para verificar se há uma relação significativa entre a hora do dia e a ocorrência de fraudes, será realizado um teste de hipótese. Para tal, é escolhido o teste do Qui-Quadrado de Independência pois pretende-se verificar a independência entre duas variáveis categóricas em uma tabela de contingência. Esse teste compara as contagens observadas de cada combinação de categorias com as contagens que seriam esperadas se as variáveis fossem independentes. Se houver uma diferença significativa entre as contagens observadas e as esperadas, o teste indicará uma possível associação entre as variáveis.
H0: Não há associação entre as variáveis categóricas hora e fraude
HA: Há associação entre as variáveis categóricas hora e fraude
É definido um nível de significância (alpha) de 0.05 para o teste.
# Criando a tabela de contingência
tabela_contingencia = pd.crosstab(df['order date (DateOrders)'].dt.hour.rename('Hora'), df['target'])
# Realizar o teste do qui-quadrado
chi2, p, dof, expected = chi2_contingency(tabela_contingencia)
print(f'O p-valor obtido foi de: {p:.6f}')
# Interpretação dos resultados
if p < 0.05:
print("Rejeitamos a hipótese nula. Há uma relação significativa entre 'Hora' e 'target'.")
else:
print("Não rejeitamos a hipótese nula. 'Hora' e 'target' parecem ser independentes.")
O p-valor obtido foi de: 0.000012 Rejeitamos a hipótese nula. Há uma relação significativa entre 'Hora' e 'target'.
Fraudes por dia¶
# Agrupando por dia e status de fraude e contando as ocorrências
fraude_por_dia = df.groupby([df['order date (DateOrders)'].dt.day.rename('Dia'), 'target']).size().reset_index(name='Contagem')
# Agora, usando o Seaborn para criar um gráfico de barras
plt.figure(figsize=(14, 8))
barplot = sns.barplot(data=fraude_por_dia, x='Dia', y='Contagem', hue='target')
# Adicionando títulos e rótulos
plt.title('Contagem de transações fraudulentas e não fraudulentas por dia do mês')
plt.xlabel('Dia do Mês')
plt.ylabel('Número de Transações')
# Ajustando os rótulos da legenda
handles, labels = barplot.get_legend_handles_labels()
barplot.legend(handles=handles, title='Status da Transação', labels=['Não Fraude', 'Fraude'])
plt.show()
Para verificar se há uma relação significativa entre o dia do mês e a ocorrência de fraudes, será realizado um teste de hipótese:
H0: Não há associação entre as variáveis categóricas dia e fraude
HA: Há associação entre as variáveis categóricas dia e fraude
É definido um nível de significância (alpha) de 0.05 para o teste.
# Criando a tabela de contingência
tabela_contingencia = pd.crosstab(df['order date (DateOrders)'].dt.day.rename('Hora'), df['target'])
# Realizar o teste do qui-quadrado
chi2, p, dof, expected = chi2_contingency(tabela_contingencia)
print(f'O p-valor obtido foi de: {p:.10f}')
# Interpretação dos resultados
if p < 0.05:
print("Rejeitamos a hipótese nula. Há uma relação significativa entre 'dia' e 'target'.")
else:
print("Não rejeitamos a hipótese nula. 'dia' e 'target' parecem ser independentes.")
O p-valor obtido foi de: 0.0000000082 Rejeitamos a hipótese nula. Há uma relação significativa entre 'dia' e 'target'.
Fraudes por mês¶
# Agrupando por mes e status de fraude e contando as ocorrências
fraude_por_mes = df.groupby([df['order date (DateOrders)'].dt.month.rename('Mes'), 'target']).size().reset_index(name='Contagem')
# Agora, usando o Seaborn para criar um gráfico de barras
plt.figure(figsize=(14, 8))
barplot = sns.barplot(data=fraude_por_mes, x='Mes', y='Contagem', hue='target')
# Adicionando títulos e rótulos
plt.title('Contagem de transações fraudulentas e não fraudulentas por meses do ano')
plt.xlabel('Mês')
plt.ylabel('Número de Transações')
# Ajustando os rótulos da legenda
handles, labels = barplot.get_legend_handles_labels()
barplot.legend(handles=handles, title='Status da Transação', labels=['Não Fraude', 'Fraude'])
plt.show()
Para verificar se há uma relação significativa entre o mês do ano e a ocorrência de fraudes, será realizado um teste de hipótese:
H0: Não há associação entre as variáveis categóricas mês e fraude
HA: Há associação entre as variáveis categóricas mês e fraude
É definido um nível de significância (alpha) de 0.05 para o teste.
# Criando a tabela de contingência
tabela_contingencia = pd.crosstab(df['order date (DateOrders)'].dt.month.rename('Mes'), df['target'])
# Realizar o teste do qui-quadrado
chi2, p, dof, expected = chi2_contingency(tabela_contingencia)
print(f'O p-valor obtido foi de: {p:.10f}')
# Interpretação dos resultados
if p < 0.05:
print("Rejeitamos a hipótese nula. Há uma relação significativa entre 'mes' e 'target'.")
else:
print("Não rejeitamos a hipótese nula. 'mes' e 'target' parecem ser independentes.")
O p-valor obtido foi de: 0.0000735397 Rejeitamos a hipótese nula. Há uma relação significativa entre 'mes' e 'target'.
7.7 Análise dos modos de entrega¶
# Obter a contagem para cada status de entrega
modos_entrega = df_filtrado['Shipping Mode'].value_counts()
modos_entrega_df = modos_entrega.reset_index()
modos_entrega_df.columns = ['Modo de Entrega', 'Quantidade de Pedidos']
fig = px.bar(modos_entrega_df, x='Modo de Entrega', y='Quantidade de Pedidos', color='Modo de Entrega',
title="Quantidade de pedidos por modo de envio", text = 'Quantidade de Pedidos')
fig.update_traces(textposition='outside', texttemplate='%{text}', textfont_size=12)
fig.update_layout(width=1000, height=500)
fig.show()
modos_entrega_tempo = df_filtrado.groupby([df['order date (DateOrders)'].dt.to_period('M'), 'Shipping Mode']).size()
modos_entrega_tempo = modos_entrega_tempo.unstack(fill_value=0)
modos_entrega_tempo.index = modos_entrega_tempo.index.to_timestamp()
fig = px.line(modos_entrega_tempo, title='Modos de Entrega ao Longo do Tempo',
labels={'value': 'Quantidade de Pedidos', 'order date (DateOrders)': 'Data', 'Shipping Mode': 'Modo de Entrega'})
fig.update_layout(width=1000, height=500)
fig.show()
# Criando a coluna que mostra se a entrega foi atrasada (1) ou não (0)
df_filtrado['Atraso'] = (df_filtrado['Days for shipping (real)'] > df_filtrado['Days for shipment (scheduled)']).astype(int)
# Agrupando por 'shipping mode' e contando atrasos
atraso_por_modo_de_entrega = df_filtrado.groupby('Shipping Mode')['Atraso'].value_counts().unstack(fill_value=0)
# Calculando a porcentagem de atrasos
atraso_por_modo_de_entrega['Entregas Totais'] = atraso_por_modo_de_entrega.sum(axis=1)
atraso_por_modo_de_entrega['Porcentagem de Atraso'] = ((atraso_por_modo_de_entrega[1] / atraso_por_modo_de_entrega['Entregas Totais']) * 100).round(2)
# Criando o dataframe final
final_df = atraso_por_modo_de_entrega.rename(columns={1: 'Entregas Atrasadas', 0: 'Entregas à Tempo'})
final_df = final_df[['Entregas Totais', 'Entregas Atrasadas', 'Porcentagem de Atraso']].reset_index()
#final_df = final_df.set_index('Atraso')
#final_df.reset_index(drop=True, inplace=True)
final_df
| Atraso | Shipping Mode | Entregas Totais | Entregas Atrasadas | Porcentagem de Atraso |
|---|---|---|---|---|
| 0 | First Class | 25270 | 25270 | 100.00 |
| 1 | Same Day | 8851 | 4223 | 47.71 |
| 2 | Second Class | 32188 | 25688 | 79.81 |
| 3 | Standard Class | 98263 | 39087 | 39.78 |
fig = px.bar(final_df, x='Shipping Mode', y='Porcentagem de Atraso', color='Shipping Mode',
title="Porcentagem de atraso por modo de envio", text = 'Porcentagem de Atraso',
labels={'Shipping Mode': 'Modo de Envio'})
fig.update_traces(textposition='outside', texttemplate='%{text}%', textfont_size=12)
fig.update_layout(width=1000, height=500)
fig.show()
Pelo gráfico acima, percebe-se que ocorreram com atraso:
- Todas (100%) as entregas do modo First Class.
- Quase metade (47,71%) das entregas do modo Same Day.
- Praticamente 80% (79,81%) das entregas do modo Second Class.
- Quase 40% (39,78%) das entregas do modo Standard Class.
tipo_pagamento = df_filtrado['Type'].value_counts()
# Tamanho do gráfico
plt.figure(figsize=(8,6))
# Cria um gráfico de barras com índice e contagem
barra = plt.bar(
tipo_pagamento.index, # valor no eixo x
tipo_pagamento.values, # valor no eixo y
color = ['steelblue', 'lightcoral', 'olive','#ff7f50'] # cores das barras
)
# Rotulo do eixo y, letra tamanho 8
plt.ylabel('Número de pedidos', fontsize = 12)
# Titulo, letra tamanho 14
plt.title('Quantidade de pedidos por método de pagamento', fontsize = 16)
# Adicionando a contagem em cima das barras
for bar in barra:
yval = bar.get_height()
plt.text(bar.get_x() + bar.get_width()/2.0, yval, int(yval), va='bottom', ha='center', fontsize=13)
plt.show()
# Porcentagem de pedidos pagos para cada modo de pagamento
df_filtrado['Type'].value_counts(1)
Type DEBIT 0.401107 TRANSFER 0.243814 PAYMENT 0.241797 CASH 0.113282 Name: proportion, dtype: float64
tipo_pagamento_mensal = df_filtrado.groupby([df['order date (DateOrders)'].dt.to_period('M'), 'Type']).size()
tipo_pagamento_mensal = tipo_pagamento_mensal.unstack(fill_value=0)
tipo_pagamento_mensal.index = tipo_pagamento_mensal.index.to_timestamp()
fig = px.line(tipo_pagamento_mensal, title='Tipos de pagamento ao longo do tempo',
labels={'value': 'Quantidade de Pedidos', 'order date (DateOrders)': 'Data', 'Type': 'Tipo de Pagamento'})
fig.update_layout(width=1000, height=500)
fig.show()
mediana_precos = df_filtrado['Order Item Product Price'].median()
mediana_descontos = df_filtrado['Order Item Discount'].median()
mediana_precos_format = (
"{:,.0f}".format(mediana_precos)
.replace(",", ".")
)
mediana_descontos_format = (
"{:,.0f}".format(mediana_descontos)
.replace(",", ".")
)
# Crie dois subplots (um para o histograma de 'preços' e outro para o histograma de 'descontos')
fig = make_subplots(rows=1, cols=2, subplot_titles=('Histograma dos preços', 'Histograma dos descontos'))
# Adicione o histograma de 'preços' ao primeiro subplot
histogram_preços = go.Histogram(x=df_filtrado['Order Item Product Price'], nbinsx=10, marker=dict(color='blue'))
fig.add_trace(histogram_preços, row=1, col=1)
# Adicione o histograma de 'descontos' ao segundo subplot
histogram_descontos = go.Histogram(x=df_filtrado['Order Item Discount'], nbinsx=12, marker=dict(color='green'))
fig.add_trace(histogram_descontos, row=1, col=2)
# Adicione a linha da mediana a ambos os subplots
line_precos = go.Scatter(x=[mediana_precos, mediana_precos], y=[0, 144844], mode='lines',
line=dict(color='lightblue', dash='dash'),
showlegend=True,
name=f"Mediana dos preços = {mediana_precos_format}")
fig.add_trace(line_precos, row=1, col=1)
line_descontos = go.Scatter(x=[mediana_descontos, mediana_descontos], y=[0, 125593], mode='lines',
line=dict(color='lightgreen', dash='dash'),
showlegend=True,
name=f"Mediana dos descontos = {mediana_descontos_format}")
fig.add_trace(line_descontos, row=1, col=2)
# Atualize o layout e as configurações dos subplots
fig.update_layout(title_text='Histograma dos preços e descontos dos produtos',
autosize=False,
width=1200, # Largura total dos subplots
height=500)
fig.update_yaxes(range=[0, 150000], row=1, col=1)
fig.update_yaxes(range=[0, 150000], row=1, col=2)
# Mostre o gráfico
fig.show()
# Define a localidade para adicionar o separador de milhares
locale.setlocale(locale.LC_ALL, '')
# Cria uma figura com 2 subplots
fig, axs = plt.subplots(1, 2, figsize=(16, 9))
# Cria um boxplot para a variável 'preço' no primeiro subplot
bp1 = axs[0].boxplot(df_filtrado['Order Item Product Price'], patch_artist=True)
axs[0].set_title('Boxplot dos preços')
# Calcula a média e mediana do preço
mean_preco = np.mean(df_filtrado['Order Item Product Price'])
median_preco = np.median(df_filtrado['Order Item Product Price'])
# Define a cor do boxplot
bp1['boxes'][0].set_facecolor('lightblue')
# Adiciona a legenda da média e mediana com separador de milhares
max_rent = np.max(df_filtrado['Order Item Product Price'])
axs[0].annotate(f'Média = {locale.format_string("%.2f", mean_preco, grouping=True)}\nMediana = {locale.format_string("%.2f", median_preco, grouping=True)}',
xy=(1, max_rent*0.8),
xytext=(1.15, max_rent*0.8),
bbox=dict(facecolor='lightblue', edgecolor='blue'),
fontsize=10)
# Cria um boxplot para a variável 'desconto' no segundo subplot
bp2 = axs[1].boxplot(df_filtrado['Order Item Discount'], patch_artist=True)
axs[1].set_title('Boxplot dos descontos')
# Calcula a média e mediana do desconto
mean_desconto = np.mean(df_filtrado['Order Item Discount'])
median_desconto = np.median(df_filtrado['Order Item Discount'])
# Define a cor do boxplot
bp2['boxes'][0].set_facecolor('lightgreen')
# Adiciona a legenda da média e mediana com separador de milhares
max_total = np.max(df_filtrado['Order Item Discount'])
axs[1].annotate(f'Média = {locale.format_string("%.2f", mean_desconto, grouping=True)}\nMediana = {locale.format_string("%.2f", median_desconto, grouping=True)}',
xy=(1, max_total*0.8),
xytext=(1.15, max_total*0.8),
bbox=dict(facecolor='lightgreen', edgecolor='green'),
fontsize=10)
# Mostra os gráficos
plt.show()
df_filtrado['Order Item Product Price'].describe()
count 164572.000000 mean 133.657817 std 117.673170 min 9.990000 25% 50.000000 50% 59.990002 75% 199.990005 max 1999.989990 Name: Order Item Product Price, dtype: float64
df_filtrado['Order Item Discount'].describe()
count 164572.000000 mean 20.201156 std 19.792135 min 0.000000 25% 5.500000 50% 14.000000 75% 29.990000 max 500.000000 Name: Order Item Discount, dtype: float64
# Assimetria
print(f"A assimetria da distribuição dos preços assume valor: {skew(df_filtrado['Order Item Product Price']):.2f}")
print(f"A assimetria da distribuição dos descontos assume valor: {skew(df_filtrado['Order Item Discount']):.2f}")
A assimetria da distribuição dos preços assume valor: 1.37 A assimetria da distribuição dos descontos assume valor: 1.74
Distribuição de preços¶
- A mediana Q2 está muito mais próxima de Q1 do que de Q3, indicando que a maioria dos preços assume um valor pequeno e que a distribuição possui assimetria à direita (calculada em 1.37), o que é acentuado pelos outliers superiores.
Distribuição de descontos¶
- A mediana Q2 está mais próxima de Q1 do que de Q3, indicando que a maioria dos descontos assume um valor pequeno e que a distribuição também possui assimetria à direita (calculada em 1.74), o que é acentuado pelos outliers superiores. Neste caso, os dados estão mais centralizados (média mais próxima da mediana) e menos dispersos (desvio padrão menor), em comparação à distribuição dos preços.
10.1 Análise dos produtos mais vendidos¶
# Calcular o valor total de vendas por item
nome_produto = df_filtrado.groupby('Product Name', as_index=False)['Sales'].sum()
nome_produto = nome_produto.rename(columns={'Sales': 'Vendas Totais', 'Product Name':'Nome do Produto'})
# Calcular o valor total de vendas de todos os itens
valor_total_vendas = df_filtrado['Sales'].sum()
# Calcular a porcentagem do valor total de vendas para cada item
nome_produto['Percentual_do_valor_de_venda'] = ((nome_produto['Vendas Totais'] / valor_total_vendas) * 100).round(2)
# Ordenar os itens por contribuição de valor descendente
nome_produto_ordenado = nome_produto.sort_values('Percentual_do_valor_de_venda', ascending=False)
# Calcular a porcentagem acumulada do valor total
nome_produto_ordenado['Percentual_acumulado'] = nome_produto_ordenado['Percentual_do_valor_de_venda'].cumsum()
nome_produto_ordenado.head(20)
| Nome do Produto | Vendas Totais | Percentual_do_valor_de_venda | Percentual_acumulado | |
|---|---|---|---|---|
| 18 | Field & Stream Sportsman 16 Gun Fire Safe | 6.631669e+06 | 20.23 | 20.23 |
| 60 | Perfect Fitness Perfect Rip Deck | 4.226596e+06 | 12.89 | 33.12 |
| 15 | Diamondback Women's Serene Classic Comfort Bi | 3.942337e+06 | 12.02 | 45.14 |
| 50 | Nike Men's Free 5.0+ Running Shoe | 3.501150e+06 | 10.68 | 55.82 |
| 48 | Nike Men's Dri-FIT Victory Golf Polo | 3.007700e+06 | 9.17 | 64.99 |
| 59 | Pelican Sunstream 100 Kayak | 2.962852e+06 | 9.04 | 74.03 |
| 56 | O'Brien Men's Neoprene Life Vest | 2.762145e+06 | 8.42 | 82.45 |
| 45 | Nike Men's CJ Elite 2 TD Football Cleat | 2.760078e+06 | 8.42 | 90.87 |
| 85 | Under Armour Girls' Toddler Spine Surge Runni | 1.213896e+06 | 3.70 | 94.57 |
| 98 | adidas Youth Germany Black/Red Away Match Soc | 6.349000e+04 | 0.19 | 94.76 |
| 35 | LIJA Women's Eyelet Sleeveless Golf Polo | 5.869500e+04 | 0.18 | 94.94 |
| 96 | adidas Men's F10 Messi TRX FG Soccer Cleat | 5.471088e+04 | 0.17 | 95.11 |
| 81 | Titleist Pro V1x High Numbers Personalized Go | 4.554324e+04 | 0.14 | 95.25 |
| 78 | Titleist Pro V1 High Numbers Personalized Gol | 4.263180e+04 | 0.13 | 95.38 |
| 46 | Nike Men's Comfort 2 Slide | 4.292046e+04 | 0.13 | 95.51 |
| 92 | Under Armour Women's Micro G Skulpt Running S | 4.419588e+04 | 0.13 | 95.64 |
| 79 | Titleist Pro V1x Golf Balls | 4.074351e+04 | 0.12 | 95.76 |
| 80 | Titleist Pro V1x High Numbers Golf Balls | 3.728823e+04 | 0.11 | 95.87 |
| 88 | Under Armour Men's Compression EV SL Slide | 3.486725e+04 | 0.11 | 95.98 |
| 11 | Clicgear Rovic Cooler Bag | 3.307173e+04 | 0.10 | 96.08 |
# Categorizando os produtos
nome_produto_ordenado['Categoria'] = 'C'
nome_produto_ordenado.loc[nome_produto_ordenado['Percentual_acumulado'] <= 80, 'Categoria'] = 'A'
nome_produto_ordenado.loc[(nome_produto_ordenado['Percentual_acumulado'] > 80) & (nome_produto_ordenado['Percentual_acumulado'] <= 95), 'Categoria'] = 'B'
nome_produto_ordenado['Vendas Totais (em milhoes)'] = (nome_produto_ordenado['Vendas Totais']/1000000).round(3)
nome_produto_ordenado = nome_produto_ordenado.drop('Vendas Totais', axis=1)
nome_produto_ordenado.head(20)
| Nome do Produto | Percentual_do_valor_de_venda | Percentual_acumulado | Categoria | Vendas Totais (em milhoes) | |
|---|---|---|---|---|---|
| 18 | Field & Stream Sportsman 16 Gun Fire Safe | 20.23 | 20.23 | A | 6.632 |
| 60 | Perfect Fitness Perfect Rip Deck | 12.89 | 33.12 | A | 4.227 |
| 15 | Diamondback Women's Serene Classic Comfort Bi | 12.02 | 45.14 | A | 3.942 |
| 50 | Nike Men's Free 5.0+ Running Shoe | 10.68 | 55.82 | A | 3.501 |
| 48 | Nike Men's Dri-FIT Victory Golf Polo | 9.17 | 64.99 | A | 3.008 |
| 59 | Pelican Sunstream 100 Kayak | 9.04 | 74.03 | A | 2.963 |
| 56 | O'Brien Men's Neoprene Life Vest | 8.42 | 82.45 | B | 2.762 |
| 45 | Nike Men's CJ Elite 2 TD Football Cleat | 8.42 | 90.87 | B | 2.760 |
| 85 | Under Armour Girls' Toddler Spine Surge Runni | 3.70 | 94.57 | B | 1.214 |
| 98 | adidas Youth Germany Black/Red Away Match Soc | 0.19 | 94.76 | B | 0.063 |
| 35 | LIJA Women's Eyelet Sleeveless Golf Polo | 0.18 | 94.94 | B | 0.059 |
| 96 | adidas Men's F10 Messi TRX FG Soccer Cleat | 0.17 | 95.11 | C | 0.055 |
| 81 | Titleist Pro V1x High Numbers Personalized Go | 0.14 | 95.25 | C | 0.046 |
| 78 | Titleist Pro V1 High Numbers Personalized Gol | 0.13 | 95.38 | C | 0.043 |
| 46 | Nike Men's Comfort 2 Slide | 0.13 | 95.51 | C | 0.043 |
| 92 | Under Armour Women's Micro G Skulpt Running S | 0.13 | 95.64 | C | 0.044 |
| 79 | Titleist Pro V1x Golf Balls | 0.12 | 95.76 | C | 0.041 |
| 80 | Titleist Pro V1x High Numbers Golf Balls | 0.11 | 95.87 | C | 0.037 |
| 88 | Under Armour Men's Compression EV SL Slide | 0.11 | 95.98 | C | 0.035 |
| 11 | Clicgear Rovic Cooler Bag | 0.10 | 96.08 | C | 0.033 |
fig = px.bar(nome_produto_ordenado[nome_produto_ordenado['Categoria'] == 'A'], x='Nome do Produto', y='Vendas Totais (em milhoes)', color='Nome do Produto',
title="Produtos que correspondem a 80% do valor total de vendas", text = 'Vendas Totais (em milhoes)')
fig.update_traces(textposition='outside', texttemplate='%{text}MM', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()
Os produtos do gráfico acima correspondem a 80% das vendas (em valores financeiros) da DataCo Global:
- Field & Stream Sportsman 16 Gun Fire Safe
- Perfect Fitness Perfect Rip Deck
- Diamondback Women's Serene Classic Comfort Bi
- Nike Men's Free 5.0+ Running Shoe
- Nike Men's Dri-FIT Victory Golf Polo
- Pelican Sunstream 100 Kayak
- Nike Men's CJ Elite 2 TD Football Cleat
# Juntando todos os nomes de produtos em uma única string
produtos_venda = " ".join(nome_produto for nome_produto in df['Product Name'])
# gerando a nuvem de palavras
wordcloud = WordCloud(background_color="white").generate(produtos_venda)
# plotando a nuvem de palavras
plt.figure(figsize=(10, 5))
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.show()
Com base nas informações encontradas, a DataCo Global poderia adotar as seguintes ações:
Campanhas Direcionadas: Concentrar esforços de marketing e vendas nos produtos mais lucrativos, utilizando campanhas direcionadas para impulsionar ainda mais as vendas desses produtos.
Pacotes e Promoções: Criar pacotes de produtos ou promoções que incluam produtos populares junto com itens menos vendidos para aumentar a exposição destes últimos.
10.2 Análise dos produtos mais lucrativos¶
# Calcular o valor total de lucro por item
nome_produto_lucro = df_filtrado.groupby('Product Name', as_index=False)['Order Profit Per Order'].sum()
nome_produto_lucro = nome_produto_lucro.rename(columns={'Order Profit Per Order': 'Lucros Totais', 'Product Name':'Nome do Produto'})
# Calcular o valor total de lucros de todos os itens
valor_total_lucros = df_filtrado['Order Profit Per Order'].sum()
# Calcular a porcentagem do valor total de lucros para cada item
nome_produto_lucro['Percentual_do_valor_de_lucro'] = ((nome_produto_lucro['Lucros Totais'] / valor_total_lucros) * 100).round(2)
# Ordenar os itens por contribuição de valor descendente
nome_produto_lucro_ordenado = nome_produto_lucro.sort_values('Percentual_do_valor_de_lucro', ascending=False)
# Calcular a porcentagem acumulada do valor total
nome_produto_lucro_ordenado['Percentual_acumulado'] = nome_produto_lucro_ordenado['Percentual_do_valor_de_lucro'].cumsum()
nome_produto_lucro_ordenado.head(20)
| Nome do Produto | Lucros Totais | Percentual_do_valor_de_lucro | Percentual_acumulado | |
|---|---|---|---|---|
| 18 | Field & Stream Sportsman 16 Gun Fire Safe | 730159.127155 | 20.68 | 20.68 |
| 60 | Perfect Fitness Perfect Rip Deck | 472867.789776 | 13.40 | 34.08 |
| 15 | Diamondback Women's Serene Classic Comfort Bi | 409215.238132 | 11.59 | 45.67 |
| 50 | Nike Men's Free 5.0+ Running Shoe | 358365.128570 | 10.15 | 55.82 |
| 48 | Nike Men's Dri-FIT Victory Golf Polo | 334162.569531 | 9.47 | 65.29 |
| 59 | Pelican Sunstream 100 Kayak | 307522.800055 | 8.71 | 74.00 |
| 56 | O'Brien Men's Neoprene Life Vest | 302190.730486 | 8.56 | 82.56 |
| 45 | Nike Men's CJ Elite 2 TD Football Cleat | 296357.120152 | 8.40 | 90.96 |
| 85 | Under Armour Girls' Toddler Spine Surge Runni | 121568.960312 | 3.44 | 94.40 |
| 96 | adidas Men's F10 Messi TRX FG Soccer Cleat | 8078.230037 | 0.23 | 94.63 |
| 98 | adidas Youth Germany Black/Red Away Match Soc | 7467.999986 | 0.21 | 94.84 |
| 81 | Titleist Pro V1x High Numbers Personalized Go | 6225.599967 | 0.18 | 95.02 |
| 35 | LIJA Women's Eyelet Sleeveless Golf Polo | 6391.460083 | 0.18 | 95.20 |
| 46 | Nike Men's Comfort 2 Slide | 5969.959987 | 0.17 | 95.37 |
| 78 | Titleist Pro V1 High Numbers Personalized Gol | 6003.729949 | 0.17 | 95.54 |
| 80 | Titleist Pro V1x High Numbers Golf Balls | 4375.630023 | 0.12 | 95.66 |
| 66 | TYR Boys' Team Digi Jammer | 4118.670000 | 0.12 | 95.78 |
| 86 | Under Armour Hustle Storm Medium Duffle Bag | 4096.680019 | 0.12 | 95.90 |
| 92 | Under Armour Women's Micro G Skulpt Running S | 4084.070001 | 0.12 | 96.02 |
| 79 | Titleist Pro V1x Golf Balls | 3493.839973 | 0.10 | 96.12 |
# Categorizando os produtos
nome_produto_lucro_ordenado['Categoria'] = 'C'
nome_produto_lucro_ordenado.loc[nome_produto_lucro_ordenado['Percentual_acumulado'] <= 80, 'Categoria'] = 'A'
nome_produto_lucro_ordenado.loc[(nome_produto_lucro_ordenado['Percentual_acumulado'] > 80) & (nome_produto_lucro_ordenado['Percentual_acumulado'] <= 95), 'Categoria'] = 'B'
nome_produto_lucro_ordenado['Lucros Totais (em milhares)'] = (nome_produto_lucro_ordenado['Lucros Totais']/1000).round(3)
nome_produto_lucro_ordenado = nome_produto_lucro_ordenado.drop('Lucros Totais', axis=1)
nome_produto_lucro_ordenado.head(20)
| Nome do Produto | Percentual_do_valor_de_lucro | Percentual_acumulado | Categoria | Lucros Totais (em milhares) | |
|---|---|---|---|---|---|
| 18 | Field & Stream Sportsman 16 Gun Fire Safe | 20.68 | 20.68 | A | 730.159 |
| 60 | Perfect Fitness Perfect Rip Deck | 13.40 | 34.08 | A | 472.868 |
| 15 | Diamondback Women's Serene Classic Comfort Bi | 11.59 | 45.67 | A | 409.215 |
| 50 | Nike Men's Free 5.0+ Running Shoe | 10.15 | 55.82 | A | 358.365 |
| 48 | Nike Men's Dri-FIT Victory Golf Polo | 9.47 | 65.29 | A | 334.163 |
| 59 | Pelican Sunstream 100 Kayak | 8.71 | 74.00 | A | 307.523 |
| 56 | O'Brien Men's Neoprene Life Vest | 8.56 | 82.56 | B | 302.191 |
| 45 | Nike Men's CJ Elite 2 TD Football Cleat | 8.40 | 90.96 | B | 296.357 |
| 85 | Under Armour Girls' Toddler Spine Surge Runni | 3.44 | 94.40 | B | 121.569 |
| 96 | adidas Men's F10 Messi TRX FG Soccer Cleat | 0.23 | 94.63 | B | 8.078 |
| 98 | adidas Youth Germany Black/Red Away Match Soc | 0.21 | 94.84 | B | 7.468 |
| 81 | Titleist Pro V1x High Numbers Personalized Go | 0.18 | 95.02 | C | 6.226 |
| 35 | LIJA Women's Eyelet Sleeveless Golf Polo | 0.18 | 95.20 | C | 6.391 |
| 46 | Nike Men's Comfort 2 Slide | 0.17 | 95.37 | C | 5.970 |
| 78 | Titleist Pro V1 High Numbers Personalized Gol | 0.17 | 95.54 | C | 6.004 |
| 80 | Titleist Pro V1x High Numbers Golf Balls | 0.12 | 95.66 | C | 4.376 |
| 66 | TYR Boys' Team Digi Jammer | 0.12 | 95.78 | C | 4.119 |
| 86 | Under Armour Hustle Storm Medium Duffle Bag | 0.12 | 95.90 | C | 4.097 |
| 92 | Under Armour Women's Micro G Skulpt Running S | 0.12 | 96.02 | C | 4.084 |
| 79 | Titleist Pro V1x Golf Balls | 0.10 | 96.12 | C | 3.494 |
fig = px.bar(nome_produto_lucro_ordenado[nome_produto_lucro_ordenado['Categoria'] == 'A'], x='Nome do Produto', y='Lucros Totais (em milhares)', color='Nome do Produto',
title="Produtos que correspondem a 80% do valor total do lucro", text = 'Lucros Totais (em milhares)')
fig.update_traces(textposition='outside', texttemplate='%{text}k', textfont_size=12)
fig.update_layout(width=1000, height=600)
fig.show()
Os produtos do gráfico acima correspondem a 80% das vendas (em valores financeiros) da DataCo Global:
- Field & Stream Sportsman 16 Gun Fire Safe
- Perfect Fitness Perfect Rip Deck
- Diamondback Women's Serene Classic Comfort Bi
- Nike Men's Free 5.0+ Running Shoe
- Nike Men's Dri-FIT Victory Golf Polo
- Pelican Sunstream 100 Kayak
- O'Brien Men's Neoprene Life Vest
Objetivo: Prever se um pedido será entregue com atraso, o que é importante para a gestão de expectativas dos clientes e planejamento logístico.
df_filtrado.info()
<class 'pandas.core.frame.DataFrame'> Index: 164572 entries, 48 to 180518 Data columns (total 55 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Type 164572 non-null object 1 Days for shipping (real) 164572 non-null int64 2 Days for shipment (scheduled) 164572 non-null int64 3 Benefit per order 164572 non-null float64 4 Sales per customer 164572 non-null float64 5 Delivery Status 164572 non-null object 6 Late_delivery_risk 164572 non-null int64 7 Category Id 164572 non-null int64 8 Category Name 164572 non-null object 9 Customer City 164572 non-null object 10 Customer Country 164572 non-null object 11 Customer Email 164572 non-null object 12 Customer Fname 164572 non-null object 13 Customer Id 164572 non-null int64 14 Customer Lname 164572 non-null object 15 Customer Password 164572 non-null object 16 Customer Segment 164572 non-null object 17 Customer State 164572 non-null object 18 Customer Street 164572 non-null object 19 Customer Zipcode 164572 non-null float64 20 Department Id 164572 non-null int64 21 Department Name 164572 non-null object 22 Latitude 164572 non-null float64 23 Longitude 164572 non-null float64 24 Market 164572 non-null object 25 Order City 164572 non-null object 26 Order Country 164572 non-null object 27 Order Customer Id 164572 non-null int64 28 order date (DateOrders) 164572 non-null datetime64[ns] 29 Order Id 164572 non-null int64 30 Order Item Cardprod Id 164572 non-null int64 31 Order Item Discount 164572 non-null float64 32 Order Item Discount Rate 164572 non-null float64 33 Order Item Id 164572 non-null int64 34 Order Item Product Price 164572 non-null float64 35 Order Item Profit Ratio 164572 non-null float64 36 Order Item Quantity 164572 non-null int64 37 Sales 164572 non-null float64 38 Order Item Total 164572 non-null float64 39 Order Profit Per Order 164572 non-null float64 40 Order Region 164572 non-null object 41 Order State 164572 non-null object 42 Order Status 164572 non-null object 43 Order Zipcode 23720 non-null float64 44 Product Card Id 164572 non-null int64 45 Product Category Id 164572 non-null int64 46 Product Description 0 non-null float64 47 Product Image 164572 non-null object 48 Product Name 164572 non-null object 49 Product Price 164572 non-null float64 50 Product Status 164572 non-null int64 51 shipping date (DateOrders) 164572 non-null object 52 Shipping Mode 164572 non-null object 53 Profit Margin 164572 non-null float64 54 Atraso 164572 non-null int32 dtypes: datetime64[ns](1), float64(16), int32(1), int64(14), object(23) memory usage: 69.7+ MB
# Criando a coluna target
df_filtrado['target'] = (df_filtrado['Days for shipping (real)'] > df_filtrado['Days for shipment (scheduled)']).astype(int)
# Removendo as colunas desnecessárias
colunas_para_remover = ['Days for shipping (real)', 'Days for shipment (scheduled)', 'Category Name', 'Customer Email', 'Customer Fname',
'Customer Id', 'Customer Lname', 'Customer Password', 'Customer Street', 'Customer Zipcode',
'Department Name', 'Latitude', 'Longitude', 'Order Customer Id', 'Order Id',
'Order Item Cardprod Id', 'Order Item Id', 'Order Profit Per Order', 'Order Zipcode', 'Product Card Id',
'Product Description', 'Product Image', 'Product Price', 'Product Status', 'shipping date (DateOrders)',
'Late_delivery_risk', 'Delivery Status', 'Profit Margin', 'Atraso', 'order date (DateOrders)']
df_filtrado.drop(columns=colunas_para_remover, inplace=True)
df_filtrado.head()
| Type | Benefit per order | Sales per customer | Category Id | Customer City | Customer Country | Customer Segment | Customer State | Department Id | Market | Order City | Order Country | Order Item Discount | Order Item Discount Rate | Order Item Product Price | Order Item Profit Ratio | Order Item Quantity | Sales | Order Item Total | Order Region | Order State | Order Status | Product Category Id | Product Name | Shipping Mode | target | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 48 | PAYMENT | -30.750000 | 115.180000 | 17 | Bayamon | Puerto Rico | Home Office | PR | 4 | Pacific Asia | Mirzapur | India | 4.8 | 0.04 | 59.990002 | -0.27 | 2 | 119.980003 | 115.180000 | South Asia | Uttar Pradesh | PENDING_PAYMENT | 17 | Perfect Fitness Perfect Rip Deck | Second Class | 1 |
| 49 | PAYMENT | -122.730003 | 79.180000 | 29 | Caguas | Puerto Rico | Home Office | PR | 5 | Pacific Asia | Bursa | Turquía | 0.8 | 0.01 | 39.990002 | -1.55 | 2 | 79.980003 | 79.180000 | West Asia | Bursa | PENDING_PAYMENT | 29 | Under Armour Girls' Toddler Spine Surge Runni | Second Class | 0 |
| 50 | PAYMENT | 33.599998 | 96.000000 | 24 | Caguas | Puerto Rico | Home Office | PR | 5 | Pacific Asia | Murray Bridge | Australia | 4.0 | 0.04 | 50.000000 | 0.35 | 2 | 100.000000 | 96.000000 | Oceania | Australia del Sur | PENDING_PAYMENT | 24 | Nike Men's Dri-FIT Victory Golf Polo | Second Class | 1 |
| 51 | PAYMENT | 24.690001 | 75.980003 | 29 | Caguas | Puerto Rico | Home Office | PR | 5 | Pacific Asia | Kartal | Turquía | 4.0 | 0.05 | 39.990002 | 0.33 | 2 | 79.980003 | 75.980003 | West Asia | Estambul | PENDING_PAYMENT | 29 | Under Armour Girls' Toddler Spine Surge Runni | Second Class | 0 |
| 52 | PAYMENT | 9.100000 | 91.000000 | 24 | Caguas | Puerto Rico | Home Office | PR | 5 | Pacific Asia | Ulan Bator | Mongolia | 9.0 | 0.09 | 50.000000 | 0.10 | 2 | 100.000000 | 91.000000 | Eastern Asia | Ulán Bator | PENDING_PAYMENT | 24 | Nike Men's Dri-FIT Victory Golf Polo | Second Class | 1 |
# Porcentagem pedidos atrasados (1) e não atrasados (0) em todo o dataset
df_filtrado['target'].value_counts(1)
target 1 0.572807 0 0.427193 Name: proportion, dtype: float64
# class weight
weights = df_filtrado.target.value_counts(1)[0]/df_filtrado.target.value_counts(1)[1]
# Divisão em X e y
X = df_filtrado.drop(columns=['target'], axis = 1)
y = df_filtrado.target
# Divisão em treino e teste
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .3, random_state = 42, stratify=y)
# Porcentagem pedidos atrasados (1) e não atrasados (0) no conjunto de treino
y_train.value_counts(1)
target 1 0.572804 0 0.427196 Name: proportion, dtype: float64
# Porcentagem pedidos atrasados (1) e não atrasados (0) no conjunto de teste
y_test.value_counts(1)
target 1 0.572815 0 0.427185 Name: proportion, dtype: float64
# Instanciando modelo XGBoost
modelo_XGBoost = XGBClassifier(n_estimators = 1000, max_depth = 8, learning_rate = 1e-3, n_jobs =-1, random_state = 0, scale_pos_weight=weights, eval_metric='error')
# Instanciando modelo LightGBM
modelo_LightGBM = LGBMClassifier(n_estimators = 1000, max_depth = 8, num_leaves = 2^8, learning_rate = 1e-3, n_jobs =-1, random_state = 0, is_unbalance=True, verbose=-1)
# Instanciando modelo catboost
modelo_CatBoost = CatBoostClassifier(n_estimators = 1000, max_depth = 8, learning_rate = 1e-3, random_state = 0, scale_pos_weight = weights, verbose = 0)
# Instanciando o modelo Balanced Random Forest
modelo_BRandom_Forest = BalancedRandomForestClassifier(n_estimators = 1000, max_depth = 8, random_state = 0, verbose = 0)
Construção de uma função de validação cruzada - Stratified K-Fold¶
# Função para aplicação da validação cruzada para obtenção das métricas dos modelos
def validacao_cruzada(X, y, modelo, k, threshold):
# Inicializando a função StratifiedKFold
folds = StratifiedKFold(n_splits=k, shuffle=True, random_state=40)
# Criando listas para armazenar os valores de precisão, revocação, acurácia, medida-F1, precision_recall_auc e roc_auc
# em cada fold
precisoes = list()
revocacoes=list()
acuracias=list()
Medida_F1=list()
precision_recall_auc=list()
rocs_auc=list()
cm_total = np.zeros((2, 2))
# Será aplicado o método "split" no objeto folds, que retornará uma lista
# com os índices das instâncias que pertencem ao conjunto de treino e
# outra com os índices das instâncias que pertencem ao conjunto de teste
for k, (train_index, test_index) in enumerate(folds.split(X,y)):
print("=-"*6 + f"Fold: {k+1}" + "-="*6)
# Dividindo os dados em treino e teste para cada um dos folds
X_train_intern, y_train_intern = X.iloc[train_index, :], y.iloc[train_index]
X_test_intern, y_test_intern = X.iloc[test_index, :], y.iloc[test_index]
# train_index e test_index: São os índices das instâncias do conjunto
# de treino e teste, respectivamente, selecionados em cada um dos folds
###########################################
############## Preprocessing ##############
###########################################
# Instanciando o CatBoost Encoder
encoder = CatBoostEncoder()
# Criando um imputer para preencher com a moda os valores faltantes de variáveis categóricas
cat_imputer = SimpleImputer(strategy='most_frequent')
# Criando um imputer para preencher com a mediana os valores faltantes de variáveis numéricas
num_imputer = SimpleImputer(strategy='median')
# Criando pipelines para variáveis categóricas e numéricas que preenche os valores faltantes
cat_pipeline = Pipeline([('encoder', encoder), ('imputer', cat_imputer)])
num_pipeline = Pipeline([('imputer', num_imputer)])
# Identifica as variáveis categóricas e numéricas
cat_cols = X_train_intern.select_dtypes(include=['object']).columns
num_cols = X_train_intern.select_dtypes(exclude=['object']).columns
# Aplicando os pipelines no conjunto de treinamento para preencher valores faltantes em colunas categóricas e numéricas
X_train_intern[cat_cols] = cat_pipeline.fit_transform(X_train_intern[cat_cols], y_train_intern)
X_train_intern[num_cols] = num_pipeline.fit_transform(X_train_intern[num_cols])
# Aplicando os pipelines ao conjunto de teste para preencher valores faltantes em colunas categóricas e numéricas
X_test_intern[cat_cols] = cat_pipeline.transform(X_test_intern[cat_cols])
X_test_intern[num_cols] = num_pipeline.transform(X_test_intern[num_cols])
# Treinando o modelo
modelo.fit(X_train_intern, y_train_intern)
# Obtendo as probabilidades de cada registro pertencer a classe 1
y_pred_proba = modelo.predict_proba(X_test_intern)[:, 1]
# Obtendo as previsões do modelo
y_pred = np.where(y_pred_proba > threshold, 1, 0)
# Calculando a precisão e revocação para determinar a precision_recall_auc
precisao, revocacao, limiares = precision_recall_curve(y_test_intern, y_pred)
# Calculando a matriz de confusão do fold
cm_total += confusion_matrix(y_test_intern, y_pred)
# Determinando as métricas para cada fold
precisao_revocacao_auc = auc(revocacao, precisao)
roc_auc = roc_auc_score(y_test_intern, y_pred)
acuracia_score = accuracy_score(y_test_intern, y_pred)
precisao_score = precision_score(y_test_intern, y_pred)
revocacao_score = recall_score(y_test_intern, y_pred)
f1score = f1_score(y_test_intern, y_pred)
# Armazenando as métricas nas listas criadas
precisoes.append(precisao_score)
revocacoes.append(revocacao_score)
precision_recall_auc.append(precisao_revocacao_auc)
rocs_auc.append(roc_auc)
acuracias.append(acuracia_score)
Medida_F1.append(f1score)
# Exibindo as métricas para cada um dos folds
print(f"Precisão: {precisao_score:.4f}")
print(f"Revocação: {revocacao_score:.4f}")
print(f"Acurácia: {acuracia_score:.4f}")
print(f"Medida F1: {f1score:.4f}")
print(f"Precision-Recall AUC: {precisao_revocacao_auc:.4f}")
print(f"ROC AUC: {roc_auc:.4f}")
# Transformando as listas em arrays para fazer operações matemáticas
precisoes = np.array(precisoes)
revocacoes = np.array(revocacoes)
precision_recall_auc = np.array(precision_recall_auc)
rocs_auc = np.array(rocs_auc)
acuracias = np.array(acuracias)
Medida_F1 = np.array(Medida_F1)
# Calculando as médias das métricas
media_revocacao = np.mean(revocacoes)
media_precisao = np.mean(precisoes)
media_acuracia = np.mean(acuracias)
media_F1 = np.mean(Medida_F1)
media_pr_AUC = np.mean(precision_recall_auc)
media_roc_AUC = np.mean(rocs_auc)
# Calculando os desvios padrão para cada métrica
std_revocacao = np.std(revocacoes)
std_precisao = np.std(precisoes)
std_acuracia = np.std(acuracias)
std_F1 = np.std(Medida_F1)
std_pr_AUC = np.std(precision_recall_auc)
std_roc_AUC = np.std(rocs_auc)
# Exibindo as médias das métricas obtidas
print()
print("=-"*6 + "Exibindo a média das métricas obtidas" + "-="*6)
print(f"Média da acurácia: {media_acuracia:.4f} +/- {std_acuracia:.4f}")
print(f"Média da revocação: {media_revocacao:.4f} +/- {std_revocacao:.4f}")
print(f"Média da precisão: {media_precisao:.4f} +/- {std_precisao:.4f}")
print(f"Média da Medida F1: {media_F1:.4f} +/- {std_F1:.4f}")
print(f"Média da ROC AUC: {media_roc_AUC:.4f} +/- {std_roc_AUC:.4f}")
print(f"Média da PR AUC: {media_pr_AUC:.4f} +/- {std_pr_AUC:.4f}")
# Plotando a matriz de confusão agregada com heatmap
plt.figure(figsize=(8, 6))
sns.heatmap(cm_total, annot=True, fmt=".0f", cmap="Blues")
plt.title("Matriz de Confusão Agregada de Todos os Folds")
plt.ylabel('Verdadeiro')
plt.xlabel('Previsto')
plt.show()
Modelo LightGBM¶
validacao_cruzada(X, y, modelo_LightGBM, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.8790 Revocação: 0.5806 Acurácia: 0.7140 Medida F1: 0.6993 Precision-Recall AUC: 0.8499 ROC AUC: 0.7367 =-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.8707 Revocação: 0.5741 Acurácia: 0.7072 Medida F1: 0.6919 Precision-Recall AUC: 0.8444 ROC AUC: 0.7299 =-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.8751 Revocação: 0.5787 Acurácia: 0.7113 Medida F1: 0.6967 Precision-Recall AUC: 0.8475 ROC AUC: 0.7339 =-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.8766 Revocação: 0.5797 Acurácia: 0.7125 Medida F1: 0.6979 Precision-Recall AUC: 0.8485 ROC AUC: 0.7351 =-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.8715 Revocação: 0.5744 Acurácia: 0.7077 Medida F1: 0.6924 Precision-Recall AUC: 0.8449 ROC AUC: 0.7304 =-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-= Média da acurácia: 0.7105 +/- 0.0027 Média da revocação: 0.5775 +/- 0.0027 Média da precisão: 0.8746 +/- 0.0031 Média da Medida F1: 0.6956 +/- 0.0029 Média da ROC AUC: 0.7332 +/- 0.0027 Média da PR AUC: 0.8470 +/- 0.0021
Modelo XGBoost¶
validacao_cruzada(X, y, modelo_XGBoost, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.8833 Revocação: 0.5780 Acurácia: 0.7145 Medida F1: 0.6987 Precision-Recall AUC: 0.8515 ROC AUC: 0.7378 =-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.8786 Revocação: 0.5657 Acurácia: 0.7064 Medida F1: 0.6882 Precision-Recall AUC: 0.8465 ROC AUC: 0.7304 =-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.8788 Revocação: 0.5771 Acurácia: 0.7122 Medida F1: 0.6967 Precision-Recall AUC: 0.8491 ROC AUC: 0.7352 =-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.8858 Revocação: 0.5709 Acurácia: 0.7121 Medida F1: 0.6943 Precision-Recall AUC: 0.8512 ROC AUC: 0.7361 =-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.8714 Revocação: 0.5788 Acurácia: 0.7098 Medida F1: 0.6956 Precision-Recall AUC: 0.8457 ROC AUC: 0.7321 =-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-= Média da acurácia: 0.7110 +/- 0.0027 Média da revocação: 0.5741 +/- 0.0050 Média da precisão: 0.8796 +/- 0.0049 Média da Medida F1: 0.6947 +/- 0.0036 Média da ROC AUC: 0.7343 +/- 0.0027 Média da PR AUC: 0.8488 +/- 0.0024
Modelo CatBoost¶
validacao_cruzada(X, y, modelo_CatBoost, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.8885 Revocação: 0.5721 Acurácia: 0.7138 Medida F1: 0.6960 Precision-Recall AUC: 0.8529 ROC AUC: 0.7379 =-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.8772 Revocação: 0.5658 Acurácia: 0.7059 Medida F1: 0.6879 Precision-Recall AUC: 0.8459 ROC AUC: 0.7298 =-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.8826 Revocação: 0.5705 Acurácia: 0.7105 Medida F1: 0.6931 Precision-Recall AUC: 0.8496 ROC AUC: 0.7344 =-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.8858 Revocação: 0.5685 Acurácia: 0.7109 Medida F1: 0.6925 Precision-Recall AUC: 0.8507 ROC AUC: 0.7351 =-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.8801 Revocação: 0.5648 Acurácia: 0.7067 Medida F1: 0.6881 Precision-Recall AUC: 0.8471 ROC AUC: 0.7308 =-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-= Média da acurácia: 0.7095 +/- 0.0029 Média da revocação: 0.5683 +/- 0.0027 Média da precisão: 0.8829 +/- 0.0040 Média da Medida F1: 0.6915 +/- 0.0031 Média da ROC AUC: 0.7336 +/- 0.0030 Média da PR AUC: 0.8492 +/- 0.0025
Modelo Balanced Random Forest¶
validacao_cruzada(X, y, modelo_BRandom_Forest, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.8828 Revocação: 0.5766 Acurácia: 0.7136 Medida F1: 0.6976 Precision-Recall AUC: 0.8510 ROC AUC: 0.7370 =-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.8778 Revocação: 0.5652 Acurácia: 0.7058 Medida F1: 0.6876 Precision-Recall AUC: 0.8460 ROC AUC: 0.7298 =-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.8819 Revocação: 0.5705 Acurácia: 0.7102 Medida F1: 0.6928 Precision-Recall AUC: 0.8492 ROC AUC: 0.7340 =-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.8857 Revocação: 0.5674 Acurácia: 0.7103 Medida F1: 0.6917 Precision-Recall AUC: 0.8505 ROC AUC: 0.7346 =-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.8795 Revocação: 0.5654 Acurácia: 0.7067 Medida F1: 0.6883 Precision-Recall AUC: 0.8469 ROC AUC: 0.7308 =-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-= Média da acurácia: 0.7093 +/- 0.0028 Média da revocação: 0.5690 +/- 0.0042 Média da precisão: 0.8816 +/- 0.0027 Média da Medida F1: 0.6916 +/- 0.0036 Média da ROC AUC: 0.7333 +/- 0.0026 Média da PR AUC: 0.8487 +/- 0.0019
Dentre todas as métricas de avaliação, será dada prioridade para a ROC AUC, que mede a capacidade do modelo distinguir entre entregas atrasadas e não atrasadas. Caso o custo de uma entrega em atraso não identificada previamente (falso negativo) seja alto, esta métrica se torna uma grande arma para reduzir custos operacionais e aumentar o lucro da empresa.
Possíveis impactos de constantes atrasos em entrega¶
1) Satisfação do Cliente
- Expectativa dos clientes: Atrasos não identificados e não comunicados podem levar à insatisfação do cliente, quebra de confiança e potencial perda de clientes.
- Reputação online: Uma entrega atrasada pode levar a avaliações negativas em redes sociais, podendo causar danos à reputação da empresa perante potenciais clientes.
2) Custos financeiros diretos
- Custos de Remessa e Logística: Atrasos podem aumentar os custos de logística, especialmente se forem necessárias medidas corretivas, como reexpedição ou entrega expressa.
- Penalidades Contratuais: Em alguns casos, atrasos nas entregas podem levar a multas ou penalidades contratuais, especialmente em negócios B2B, onde os contratos são mais rigorosos.
3) Implicações de Longo Prazo
- Relações com Clientes B2B: No caso de clientes empresariais, os atrasos nas entregas podem interromper suas operações, prejudicando a relação comercial de longo prazo, consequentemente as fontes de receita ao longo prazo.
- Efeito Cascata na Cadeia de Suprimentos: Atrasos em um nó da cadeia podem afetar outros, especialmente em um modelo just-in-time, onde os produtos são produzidos ou entregues exatamente quando necessários.
4) Estratégias Competitivas
- Perda de Vantagem Competitiva: Em um mercado global, a capacidade de entregar no prazo pode ser um diferencial competitivo. Atrasos frequentes podem abrir margem para o crescimento de concorrentes.
- Perda de Mercado: Clientes insatisfeitos podem se voltar para concorrentes com histórico de entregas mais confiáveis.
Sendo assim, considerando o impacto financeiro e operacional dos atrasos em entregas, é fundamental para uma empresa de supply chain como a DataCo Global, com presença global, ter uma logística capaz de cumprir com prazos e metas.
Na modelagem de previsão, a priorização da ROC AUC é uma estratégia-chave para isso. Com uma ROC AUC elevada, a empresa consegue melhor classificar entre pedidos com alta probabilidade de atraso e aqueles que provavelmente serão entregues no prazo, permitindo-lhes tomar medidas para mitigar os impactos negativos. Isso pode incluir desde a comunicação antecipada com os clientes sobre potenciais atrasos, reajustes na logística para acelerar entregas subsequentes, ou alterações na gestão do estoque para lidar com possíveis interrupções na cadeia de suprimentos.
Dentre os modelos escolhidos para avaliar a métrica ROC AUC, o XGBoost foi o que melhor performou, com uma ROC AUC média de 0.7344, ou seja, significa que existe 73,44% de chance de que o modelo classifique corretamente um pedido aleatório atrasado como atrasado e um pedido aleatório não atrasado como não atrasado.
Como o LightGBM atingiu uma ROC AUC praticamente idêntica a do XGBoost e, possui um processamento mais rápido que o XGBoost, será escolhido o LightGBM para passar por um processo de tunagem de hiperparâmetros.
Feature Selection¶
# Inicializando o RFE
rfe = RFE(estimator = modelo_LightGBM, n_features_to_select = 20, step = 1)
encoder = CatBoostEncoder()
# Ajustar e transformar os dados de treinamento
X_train_encoded = encoder.fit_transform(X_train, y_train)
# Transformar os dados de teste
X_test_encoded = encoder.transform(X_test)
# Treinando o RFE
rfe.fit(X_train_encoded, y_train)
# Obtendo as features selecionadas
features_importantes = np.array(list(X_train_encoded.columns))[rfe.support_]
features_importantes
array(['Type', 'Benefit per order', 'Sales per customer', 'Category Id',
'Customer City', 'Customer Country', 'Customer Segment',
'Customer State', 'Department Id', 'Order City',
'Order Item Profit Ratio', 'Order Item Quantity', 'Sales',
'Order Item Total', 'Order Region', 'Order State', 'Order Status',
'Product Category Id', 'Product Name', 'Shipping Mode'],
dtype='<U24')
validacao_cruzada(X[features_importantes], y, modelo_LightGBM, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.8790 Revocação: 0.5806 Acurácia: 0.7140 Medida F1: 0.6993 Precision-Recall AUC: 0.8499 ROC AUC: 0.7367 =-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.8707 Revocação: 0.5741 Acurácia: 0.7072 Medida F1: 0.6919 Precision-Recall AUC: 0.8444 ROC AUC: 0.7299 =-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.8751 Revocação: 0.5787 Acurácia: 0.7113 Medida F1: 0.6967 Precision-Recall AUC: 0.8475 ROC AUC: 0.7339 =-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.8766 Revocação: 0.5797 Acurácia: 0.7125 Medida F1: 0.6979 Precision-Recall AUC: 0.8485 ROC AUC: 0.7351 =-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.8715 Revocação: 0.5744 Acurácia: 0.7077 Medida F1: 0.6924 Precision-Recall AUC: 0.8449 ROC AUC: 0.7304 =-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-= Média da acurácia: 0.7105 +/- 0.0027 Média da revocação: 0.5775 +/- 0.0027 Média da precisão: 0.8746 +/- 0.0031 Média da Medida F1: 0.6956 +/- 0.0029 Média da ROC AUC: 0.7332 +/- 0.0027 Média da PR AUC: 0.8470 +/- 0.0021
Neste caso, realizando uma feature selection e excluindo as 5 colunas menos importantes para o modelos, atingimos o mesmo resultado que com o dataset original. Assim, por questões de processamento, é preferível manter o dataset com as 5 colunas a menos.
Tunagem de Hiperparâmetros¶
def tunagem_hiperparametros(trial, k = 5, threshold = 0.5):
# Parâmetros para serem tunados
learning_rate = trial.suggest_float('learning_rate', 1e-3, 1e-1, log=True)
max_depth = trial.suggest_int('max_depth', 1, 20)
subsample = trial.suggest_float('subsample', 0.5, 1, step = 0.1)
colsample_bytree = trial.suggest_float('colsample_bytree', 0.5, 1, step = 0.1)
min_child_samples = trial.suggest_int('min_child_samples', 1, 20)
min_child_weight = trial.suggest_float('min_child_weight', 1e-3, 1e-1)
# Inicializando a função StratifiedKFold
folds = StratifiedKFold(n_splits=k, shuffle=True, random_state=42)
# Criando listas para armazenar os valores de precisão, revocação, acurácia, medida-F1, precision_recall_auc e roc_auc
# em cada fold
precisoes = list()
revocacoes=list()
acuracias=list()
Medida_F1=list()
precision_recall_auc=list()
rocs_auc=list()
# Será aplicado o método "split" no objeto folds, que retornará uma lista
# com os índices das instâncias que pertencem ao conjunto de treino e
# outra com os índices das instâncias que pertencem ao conjunto de teste
for k, (train_index, test_index) in enumerate(folds.split(X[features_importantes],y)):
print("=-"*6 + f"Fold: {k+1}" + "-="*6)
# Dividindo os dados em treino e teste para cada um dos folds
X_train_intern, y_train_intern = X[features_importantes].iloc[train_index, :], y.iloc[train_index]
X_test_intern, y_test_intern = X[features_importantes].iloc[test_index, :], y.iloc[test_index]
# train_index e test_index: São os índices das instâncias do conjunto
# de treino e teste, respectivamente, selecionados em cada um dos folds
###########################################
############## Preprocessing ##############
###########################################
# Instanciando o CatBoost Encoder
encoder = CatBoostEncoder()
# Criando um imputer para preencher com a moda os valores faltantes de variáveis categóricas
cat_imputer = SimpleImputer(strategy='most_frequent')
# Criando um imputer para preencher com a mediana os valores faltantes de variáveis numéricas
num_imputer = SimpleImputer(strategy='median')
# Criando pipelines para variáveis categóricas e numéricas que preenche os valores faltantes
cat_pipeline = Pipeline([('encoder', encoder), ('imputer', cat_imputer)])
num_pipeline = Pipeline([('imputer', num_imputer)])
# Identifica as variáveis categóricas e numéricas
cat_cols = X_train_intern.select_dtypes(include=['object']).columns
num_cols = X_train_intern.select_dtypes(exclude=['object']).columns
# Aplicando os pipelines no conjunto de treinamento para preencher valores faltantes em colunas categóricas e numéricas
X_train_intern[cat_cols] = cat_pipeline.fit_transform(X_train_intern[cat_cols], y_train_intern)
X_train_intern[num_cols] = num_pipeline.fit_transform(X_train_intern[num_cols])
# Aplicando os pipelines ao conjunto de teste para preencher valores faltantes em colunas categóricas e numéricas
X_test_intern[cat_cols] = cat_pipeline.transform(X_test_intern[cat_cols])
X_test_intern[num_cols] = num_pipeline.transform(X_test_intern[num_cols])
# Instanciando o Modelo LightGBM
modelo_LightGBM = LGBMClassifier(n_estimators = 1000, max_depth = max_depth, num_leaves = 2^8, subsample = subsample,
min_child_weight = min_child_weight, min_child_samples = min_child_samples,
colsample_bytree = colsample_bytree, learning_rate = learning_rate, n_jobs =-1,
random_state = 0, is_unbalance=True, verbose=-1)
# Treinando o modelo LightGBM
modelo_LightGBM.fit(X_train_intern, y_train_intern)
# Obtendo as probabilidades de cada registro pertencer a classe 1
y_pred_proba = modelo_LightGBM.predict_proba(X_test_intern)[:, 1]
# Obtendo as previsões do modelo
y_pred = np.where(y_pred_proba > threshold, 1, 0)
# Calculando a precisão e revocação para determinar a precision_recall_auc
precisao, revocacao, limiares = precision_recall_curve(y_test_intern, y_pred)
# Determinando as métricas para cada fold
precisao_revocacao_auc = auc(revocacao, precisao)
roc_auc = roc_auc_score(y_test_intern, y_pred)
acuracia_score = accuracy_score(y_test_intern, y_pred)
precisao_score = precision_score(y_test_intern, y_pred)
revocacao_score = recall_score(y_test_intern, y_pred)
f1score = f1_score(y_test_intern, y_pred)
# Armazenando as métricas nas listas criadas
precisoes.append(precisao_score)
revocacoes.append(revocacao_score)
precision_recall_auc.append(precisao_revocacao_auc)
rocs_auc.append(roc_auc)
acuracias.append(acuracia_score)
Medida_F1.append(f1score)
# Transformando as listas em arrays para fazer operações matemáticas
precisoes = np.array(precisoes)
revocacoes = np.array(revocacoes)
precision_recall_auc = np.array(precision_recall_auc)
rocs_auc = np.array(rocs_auc)
acuracias = np.array(acuracias)
Medida_F1 = np.array(Medida_F1)
# Calculando as médias das métricas
media_revocacao = np.mean(revocacoes)
media_precisao = np.mean(precisoes)
media_acuracia = np.mean(acuracias)
media_F1 = np.mean(Medida_F1)
media_pr_AUC = np.mean(precision_recall_auc)
media_roc_AUC = np.mean(rocs_auc)
# Calculando os desvios padrão para cada métrica
std_revocacao = np.std(revocacoes)
std_precisao = np.std(precisoes)
std_acuracia = np.std(acuracias)
std_F1 = np.std(Medida_F1)
std_pr_AUC = np.std(precision_recall_auc)
std_roc_AUC = np.std(rocs_auc)
return media_roc_AUC
study = opt.create_study(direction='maximize')
study.optimize(tunagem_hiperparametros, n_trials = 20)
[I 2024-02-10 22:09:26,049] A new study created in memory with name: no-name-7a27c5dc-b174-4b96-9e7d-b473b524cd28
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:10:42,568] Trial 0 finished with value: 0.732976869219396 and parameters: {'learning_rate': 0.0020404155935778907, 'max_depth': 12, 'subsample': 0.5, 'colsample_bytree': 1.0, 'min_child_samples': 8, 'min_child_weight': 0.09324004081596833}. Best is trial 0 with value: 0.732976869219396.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:11:39,538] Trial 1 finished with value: 0.7339915999812193 and parameters: {'learning_rate': 0.011507553799106215, 'max_depth': 5, 'subsample': 0.6, 'colsample_bytree': 0.6, 'min_child_samples': 4, 'min_child_weight': 0.06489413252586655}. Best is trial 1 with value: 0.7339915999812193.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:12:35,621] Trial 2 finished with value: 0.7343121783678607 and parameters: {'learning_rate': 0.0668214382430312, 'max_depth': 13, 'subsample': 0.5, 'colsample_bytree': 0.5, 'min_child_samples': 2, 'min_child_weight': 0.054893193695231275}. Best is trial 2 with value: 0.7343121783678607.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:13:37,662] Trial 3 finished with value: 0.7341373624562854 and parameters: {'learning_rate': 0.027897146878269743, 'max_depth': 14, 'subsample': 0.7, 'colsample_bytree': 0.9, 'min_child_samples': 12, 'min_child_weight': 0.09311420300985243}. Best is trial 2 with value: 0.7343121783678607.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:14:34,324] Trial 4 finished with value: 0.7343392890177047 and parameters: {'learning_rate': 0.05139440060878258, 'max_depth': 9, 'subsample': 0.6, 'colsample_bytree': 0.5, 'min_child_samples': 15, 'min_child_weight': 0.05113215031321493}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:15:27,249] Trial 5 finished with value: 0.7343173450751701 and parameters: {'learning_rate': 0.041001003249000106, 'max_depth': 9, 'subsample': 1.0, 'colsample_bytree': 0.7, 'min_child_samples': 5, 'min_child_weight': 0.009030050793958331}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:16:07,893] Trial 6 finished with value: 0.7340343882613765 and parameters: {'learning_rate': 0.07323321762143926, 'max_depth': 2, 'subsample': 0.8, 'colsample_bytree': 0.6, 'min_child_samples': 14, 'min_child_weight': 0.0212868551464531}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:17:09,167] Trial 7 finished with value: 0.7333645247162839 and parameters: {'learning_rate': 0.0024513249648152264, 'max_depth': 4, 'subsample': 0.8, 'colsample_bytree': 0.9, 'min_child_samples': 1, 'min_child_weight': 0.06107187658125102}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:18:06,027] Trial 8 finished with value: 0.734043454313138 and parameters: {'learning_rate': 0.026373724315410213, 'max_depth': 12, 'subsample': 1.0, 'colsample_bytree': 0.9, 'min_child_samples': 9, 'min_child_weight': 0.03775035246208278}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:18:59,343] Trial 9 finished with value: 0.7341953321962944 and parameters: {'learning_rate': 0.03235637623662865, 'max_depth': 9, 'subsample': 0.5, 'colsample_bytree': 0.5, 'min_child_samples': 1, 'min_child_weight': 0.016465912415566535}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:20:01,351] Trial 10 finished with value: 0.7339768956495942 and parameters: {'learning_rate': 0.007877859342979632, 'max_depth': 19, 'subsample': 0.7, 'colsample_bytree': 0.7, 'min_child_samples': 20, 'min_child_weight': 0.040042373755256974}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:21:01,827] Trial 11 finished with value: 0.734069803750568 and parameters: {'learning_rate': 0.09502736701688062, 'max_depth': 8, 'subsample': 1.0, 'colsample_bytree': 0.7, 'min_child_samples': 16, 'min_child_weight': 0.002451776114696984}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:22:01,092] Trial 12 finished with value: 0.7341169736666421 and parameters: {'learning_rate': 0.013739306362388414, 'max_depth': 7, 'subsample': 0.9, 'colsample_bytree': 0.6, 'min_child_samples': 6, 'min_child_weight': 0.07325873711793982}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:23:11,248] Trial 13 finished with value: 0.7342176615957454 and parameters: {'learning_rate': 0.044968600068135314, 'max_depth': 16, 'subsample': 0.6, 'colsample_bytree': 0.8, 'min_child_samples': 18, 'min_child_weight': 0.03486072970401499}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:24:35,996] Trial 14 finished with value: 0.7338019752137341 and parameters: {'learning_rate': 0.005228658800279436, 'max_depth': 10, 'subsample': 0.9, 'colsample_bytree': 0.5, 'min_child_samples': 12, 'min_child_weight': 0.07605837304718703}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:26:05,548] Trial 15 finished with value: 0.7339278495596633 and parameters: {'learning_rate': 0.01716729776084651, 'max_depth': 5, 'subsample': 0.6, 'colsample_bytree': 0.8, 'min_child_samples': 15, 'min_child_weight': 0.0013061193531905407}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:27:21,225] Trial 16 finished with value: 0.7341814782844065 and parameters: {'learning_rate': 0.045960965964675755, 'max_depth': 16, 'subsample': 0.9, 'colsample_bytree': 0.7, 'min_child_samples': 6, 'min_child_weight': 0.022016104377459617}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:28:45,591] Trial 17 finished with value: 0.7332943864225296 and parameters: {'learning_rate': 0.001146775199694044, 'max_depth': 7, 'subsample': 0.7, 'colsample_bytree': 0.6, 'min_child_samples': 10, 'min_child_weight': 0.04595357772517577}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:29:42,792] Trial 18 finished with value: 0.7340324656424106 and parameters: {'learning_rate': 0.020399245076261327, 'max_depth': 2, 'subsample': 0.8, 'colsample_bytree': 0.8, 'min_child_samples': 12, 'min_child_weight': 0.0290828869584896}. Best is trial 4 with value: 0.7343392890177047.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:30:47,842] Trial 19 finished with value: 0.7338228285426818 and parameters: {'learning_rate': 0.007093739073996733, 'max_depth': 10, 'subsample': 1.0, 'colsample_bytree': 0.5, 'min_child_samples': 7, 'min_child_weight': 0.011942887474184281}. Best is trial 4 with value: 0.7343392890177047.
# Melhores parâmetros obtidos do último Trial
params = {'learning_rate': 0.06672510713241127, 'max_depth': 16, 'subsample': 1.0, 'colsample_bytree': 0.6, 'min_child_samples': 12, 'min_child_weight': 0.034638755930084086}
# LightGBM executado para os melhores parâmetros
modelo_LightGBM = LGBMClassifier(n_estimators = 1000, num_leaves = 2^8, n_jobs =-1, random_state = 0, is_unbalance=True, **params, verbose=-1)
# Métricas do LightGBM utilizando os melhores parâmetros
validacao_cruzada(X, y, modelo_LightGBM, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.8859 Revocação: 0.5764 Acurácia: 0.7148 Medida F1: 0.6984 Precision-Recall AUC: 0.8525 ROC AUC: 0.7384 =-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.8788 Revocação: 0.5643 Acurácia: 0.7058 Medida F1: 0.6873 Precision-Recall AUC: 0.8463 ROC AUC: 0.7300 =-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.8830 Revocação: 0.5718 Acurácia: 0.7113 Medida F1: 0.6941 Precision-Recall AUC: 0.8501 ROC AUC: 0.7351 =-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.8833 Revocação: 0.5725 Acurácia: 0.7118 Medida F1: 0.6948 Precision-Recall AUC: 0.8503 ROC AUC: 0.7356 =-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.8840 Revocação: 0.5633 Acurácia: 0.7075 Medida F1: 0.6881 Precision-Recall AUC: 0.8487 ROC AUC: 0.7321 =-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-= Média da acurácia: 0.7103 +/- 0.0032 Média da revocação: 0.5697 +/- 0.0050 Média da precisão: 0.8830 +/- 0.0023 Média da Medida F1: 0.6925 +/- 0.0042 Média da ROC AUC: 0.7342 +/- 0.0029 Média da PR AUC: 0.8496 +/- 0.0020
Após 20 iterações de tunagem de hiperparâmetros utilizando uma Bayesian Search, a ROC AUC média do modelo LightGBM melhorou pouco, de 73,32% para 73,41%. Sendo assim, tornou-se ainda melhor na identificação de pedidos com potencial entrega atrasada, ajudando a DataCo Global na identiicação precoce destes casos, contribuindo para mitigar os riscos decorrentes de problemas de logística.
Próximos passos: para melhorar ainda mais o desempenho do modelo, pode-se implementar técnicas de feature engineering, ou seja, criando outras variáveis à partir das já existentes, para buscar encontrar fatores que influenciem positivamente o modelo.
Objetivo: Prever se um pedido poderá ser identificado como fraude, que é importante para evitar perdas financeiras à empresa.
df.info()
<class 'pandas.core.frame.DataFrame'> Index: 171962 entries, 48 to 180518 Data columns (total 54 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Type 171962 non-null object 1 Days for shipping (real) 171962 non-null int64 2 Days for shipment (scheduled) 171962 non-null int64 3 Benefit per order 171962 non-null float64 4 Sales per customer 171962 non-null float64 5 Delivery Status 171962 non-null object 6 Late_delivery_risk 171962 non-null int64 7 Category Id 171962 non-null int64 8 Category Name 171962 non-null object 9 Customer City 171962 non-null object 10 Customer Country 171962 non-null object 11 Customer Email 171962 non-null object 12 Customer Fname 171962 non-null object 13 Customer Id 171962 non-null int64 14 Customer Lname 171962 non-null object 15 Customer Password 171962 non-null object 16 Customer Segment 171962 non-null object 17 Customer State 171962 non-null object 18 Customer Street 171962 non-null object 19 Customer Zipcode 171962 non-null float64 20 Department Id 171962 non-null int64 21 Department Name 171962 non-null object 22 Latitude 171962 non-null float64 23 Longitude 171962 non-null float64 24 Market 171962 non-null object 25 Order City 171962 non-null object 26 Order Country 171962 non-null object 27 Order Customer Id 171962 non-null int64 28 order date (DateOrders) 171962 non-null datetime64[ns] 29 Order Id 171962 non-null int64 30 Order Item Cardprod Id 171962 non-null int64 31 Order Item Discount 171962 non-null float64 32 Order Item Discount Rate 171962 non-null float64 33 Order Item Id 171962 non-null int64 34 Order Item Product Price 171962 non-null float64 35 Order Item Profit Ratio 171962 non-null float64 36 Order Item Quantity 171962 non-null int64 37 Sales 171962 non-null float64 38 Order Item Total 171962 non-null float64 39 Order Profit Per Order 171962 non-null float64 40 Order Region 171962 non-null object 41 Order State 171962 non-null object 42 Order Status 171962 non-null object 43 Order Zipcode 24840 non-null float64 44 Product Card Id 171962 non-null int64 45 Product Category Id 171962 non-null int64 46 Product Description 0 non-null float64 47 Product Image 171962 non-null object 48 Product Name 171962 non-null object 49 Product Price 171962 non-null float64 50 Product Status 171962 non-null int64 51 shipping date (DateOrders) 171962 non-null object 52 Shipping Mode 171962 non-null object 53 target 171962 non-null int64 dtypes: datetime64[ns](1), float64(15), int64(15), object(23) memory usage: 76.2+ MB
# Criando a coluna target
df['target'] = df['Order Status'].apply(lambda x: 1 if x == 'SUSPECTED_FRAUD' else 0)
df.head()
| Type | Days for shipping (real) | Days for shipment (scheduled) | Benefit per order | Sales per customer | Delivery Status | Late_delivery_risk | Category Id | Category Name | Customer City | Customer Country | Customer Email | Customer Fname | Customer Id | Customer Lname | Customer Password | Customer Segment | Customer State | Customer Street | Customer Zipcode | Department Id | Department Name | Latitude | Longitude | Market | Order City | Order Country | Order Customer Id | order date (DateOrders) | Order Id | Order Item Cardprod Id | Order Item Discount | Order Item Discount Rate | Order Item Id | Order Item Product Price | Order Item Profit Ratio | Order Item Quantity | Sales | Order Item Total | Order Profit Per Order | Order Region | Order State | Order Status | Order Zipcode | Product Card Id | Product Category Id | Product Description | Product Image | Product Name | Product Price | Product Status | shipping date (DateOrders) | Shipping Mode | target | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 48 | PAYMENT | 5 | 2 | -30.750000 | 115.180000 | Late delivery | 1 | 17 | Cleats | Bayamon | Puerto Rico | XXXXXXXXX | Mary | 9083 | Frank | XXXXXXXXX | Home Office | PR | 75 Sunny Grounds | 957.0 | 4 | Apparel | 18.380119 | -66.183128 | Pacific Asia | Mirzapur | India | 9083 | 2016-02-24 13:57:00 | 28744 | 365 | 4.8 | 0.04 | 71956 | 59.990002 | -0.27 | 2 | 119.980003 | 115.180000 | -30.750000 | South Asia | Uttar Pradesh | PENDING_PAYMENT | NaN | 365 | 17 | NaN | http://images.acmesports.sports/Perfect+Fitness+Perfect+Rip+Deck | Perfect Fitness Perfect Rip Deck | 59.990002 | 0 | 2/29/2016 13:57 | Second Class | 0 |
| 49 | PAYMENT | 2 | 2 | -122.730003 | 79.180000 | Shipping on time | 0 | 29 | Shop By Sport | Caguas | Puerto Rico | XXXXXXXXX | Mary | 4741 | Smith | XXXXXXXXX | Home Office | PR | 9731 Honey Fox Towers | 725.0 | 5 | Golf | 18.235573 | -66.370613 | Pacific Asia | Bursa | Turquía | 4741 | 2016-10-25 14:39:00 | 45461 | 627 | 0.8 | 0.01 | 113598 | 39.990002 | -1.55 | 2 | 79.980003 | 79.180000 | -122.730003 | West Asia | Bursa | PENDING_PAYMENT | NaN | 627 | 29 | NaN | http://images.acmesports.sports/Under+Armour+Girls%27+Toddler+Spine+Surge+Running+Shoe | Under Armour Girls' Toddler Spine Surge Runni | 39.990002 | 0 | 10/27/2016 14:39 | Second Class | 0 |
| 50 | PAYMENT | 6 | 2 | 33.599998 | 96.000000 | Late delivery | 1 | 24 | Women's Apparel | Caguas | Puerto Rico | XXXXXXXXX | Elizabeth | 639 | Pittman | XXXXXXXXX | Home Office | PR | 7573 Golden Treasure Centre | 725.0 | 5 | Golf | 18.025368 | -66.613037 | Pacific Asia | Murray Bridge | Australia | 639 | 2016-03-30 04:37:00 | 31115 | 502 | 4.0 | 0.04 | 77757 | 50.000000 | 0.35 | 2 | 100.000000 | 96.000000 | 33.599998 | Oceania | Australia del Sur | PENDING_PAYMENT | NaN | 502 | 24 | NaN | http://images.acmesports.sports/Nike+Men%27s+Dri-FIT+Victory+Golf+Polo | Nike Men's Dri-FIT Victory Golf Polo | 50.000000 | 0 | 4/5/2016 4:37 | Second Class | 0 |
| 51 | PAYMENT | 2 | 2 | 24.690001 | 75.980003 | Shipping on time | 0 | 29 | Shop By Sport | Caguas | Puerto Rico | XXXXXXXXX | Katherine | 9702 | Tyler | XXXXXXXXX | Home Office | PR | 8369 Sunny Crossing | 725.0 | 5 | Golf | 18.273838 | -66.370636 | Pacific Asia | Kartal | Turquía | 9702 | 2016-10-30 01:31:00 | 45766 | 627 | 4.0 | 0.05 | 114401 | 39.990002 | 0.33 | 2 | 79.980003 | 75.980003 | 24.690001 | West Asia | Estambul | PENDING_PAYMENT | NaN | 627 | 29 | NaN | http://images.acmesports.sports/Under+Armour+Girls%27+Toddler+Spine+Surge+Running+Shoe | Under Armour Girls' Toddler Spine Surge Runni | 39.990002 | 0 | 11/1/2016 1:31 | Second Class | 0 |
| 52 | PAYMENT | 3 | 2 | 9.100000 | 91.000000 | Late delivery | 1 | 24 | Women's Apparel | Caguas | Puerto Rico | XXXXXXXXX | Mary | 9114 | Smith | XXXXXXXXX | Home Office | PR | 1425 Fallen Fox Arbor | 725.0 | 5 | Golf | 18.284805 | -66.370590 | Pacific Asia | Ulan Bator | Mongolia | 9114 | 2016-11-28 01:18:00 | 47752 | 502 | 9.0 | 0.09 | 119405 | 50.000000 | 0.10 | 2 | 100.000000 | 91.000000 | 9.100000 | Eastern Asia | Ulán Bator | PENDING_PAYMENT | NaN | 502 | 24 | NaN | http://images.acmesports.sports/Nike+Men%27s+Dri-FIT+Victory+Golf+Polo | Nike Men's Dri-FIT Victory Golf Polo | 50.000000 | 0 | 12/1/2016 1:18 | Second Class | 0 |
# Removendo as colunas desnecessárias
colunas_para_remover = ['Days for shipping (real)', 'Days for shipment (scheduled)', 'Category Name', 'Customer Email', 'Customer Fname',
'Customer Id', 'Customer Lname', 'Customer Password', 'Customer Street', 'Customer Zipcode',
'Department Name', 'Latitude', 'Longitude', 'Order Customer Id', 'Order Id',
'Order Item Cardprod Id', 'Order Item Id', 'Order Profit Per Order', 'Order Zipcode', 'Product Card Id',
'Product Description', 'Product Image', 'Product Status', 'shipping date (DateOrders)', 'order date (DateOrders)',
'Late_delivery_risk', 'Delivery Status', 'Order Status']
df.drop(columns=colunas_para_remover, inplace=True)
df.head()
| Type | Benefit per order | Sales per customer | Category Id | Customer City | Customer Country | Customer Segment | Customer State | Department Id | Market | Order City | Order Country | Order Item Discount | Order Item Discount Rate | Order Item Product Price | Order Item Profit Ratio | Order Item Quantity | Sales | Order Item Total | Order Region | Order State | Product Category Id | Product Name | Product Price | Shipping Mode | target | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 48 | PAYMENT | -30.750000 | 115.180000 | 17 | Bayamon | Puerto Rico | Home Office | PR | 4 | Pacific Asia | Mirzapur | India | 4.8 | 0.04 | 59.990002 | -0.27 | 2 | 119.980003 | 115.180000 | South Asia | Uttar Pradesh | 17 | Perfect Fitness Perfect Rip Deck | 59.990002 | Second Class | 0 |
| 49 | PAYMENT | -122.730003 | 79.180000 | 29 | Caguas | Puerto Rico | Home Office | PR | 5 | Pacific Asia | Bursa | Turquía | 0.8 | 0.01 | 39.990002 | -1.55 | 2 | 79.980003 | 79.180000 | West Asia | Bursa | 29 | Under Armour Girls' Toddler Spine Surge Runni | 39.990002 | Second Class | 0 |
| 50 | PAYMENT | 33.599998 | 96.000000 | 24 | Caguas | Puerto Rico | Home Office | PR | 5 | Pacific Asia | Murray Bridge | Australia | 4.0 | 0.04 | 50.000000 | 0.35 | 2 | 100.000000 | 96.000000 | Oceania | Australia del Sur | 24 | Nike Men's Dri-FIT Victory Golf Polo | 50.000000 | Second Class | 0 |
| 51 | PAYMENT | 24.690001 | 75.980003 | 29 | Caguas | Puerto Rico | Home Office | PR | 5 | Pacific Asia | Kartal | Turquía | 4.0 | 0.05 | 39.990002 | 0.33 | 2 | 79.980003 | 75.980003 | West Asia | Estambul | 29 | Under Armour Girls' Toddler Spine Surge Runni | 39.990002 | Second Class | 0 |
| 52 | PAYMENT | 9.100000 | 91.000000 | 24 | Caguas | Puerto Rico | Home Office | PR | 5 | Pacific Asia | Ulan Bator | Mongolia | 9.0 | 0.09 | 50.000000 | 0.10 | 2 | 100.000000 | 91.000000 | Eastern Asia | Ulán Bator | 24 | Nike Men's Dri-FIT Victory Golf Polo | 50.000000 | Second Class | 0 |
# Porcentagem de fraude (1) e não fraude (0) em todo o dataset
df['target'].value_counts(1)
target 0 0.977454 1 0.022546 Name: proportion, dtype: float64
fraude_ou_nao = df['target'].value_counts()
# Tamanho do gráfico
plt.figure(figsize=(8,6))
# Cria um gráfico de barras com índice e contagem
barra = plt.bar(
[0, 1], # valor no eixo x
fraude_ou_nao.values, # valor no eixo y
color = ['steelblue', 'lightcoral'] # cores das barras
)
# Define os rótulos do eixo x para 'Não Fraude' e 'Fraude'
plt.xticks([0, 1], ['Não Fraude', 'Fraude'])
# Rotulo do eixo y, letra tamanho 8
plt.ylabel('Número de pedidos', fontsize = 12)
# Titulo, letra tamanho 14
plt.title('Quantidade de operações fraudulentas', fontsize = 16)
#plt.xticks(fraude_ou_nao.index.astype(str), fraude_ou_nao.index.astype(str))
# Adicionando a contagem em cima das barras
for bar in barra:
yval = bar.get_height()
plt.text(bar.get_x() + bar.get_width()/2.0, yval, int(yval), va='bottom', ha='center', fontsize=13)
plt.grid(False)
plt.show()
Percebe-se que o conjunto de dados é extremamente desbalanceado, contendo 97.7% de operações não fraudulentas e apenas 2.3% de operações fraudulentas.
# class weight
weights = df.target.value_counts(1)[0]/df.target.value_counts(1)[1]
# Divisão em X e y
X = df.drop(columns=['target'], axis = 1)
y = df.target
# Divisão em treino e teste
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .3, random_state = 42, stratify=y)
# Porcentagem de fraude (1) e não fraude (0) no conjunto de treino
y_train.value_counts(1)
target 0 0.977453 1 0.022547 Name: proportion, dtype: float64
# Porcentagem de fraude (1) e não fraude (0) no conjunto de teste
y_test.value_counts(1)
target 0 0.977456 1 0.022544 Name: proportion, dtype: float64
# Instanciando modelo XGBoost
modelo_XGBoost = XGBClassifier(n_estimators = 1000, max_depth = 8, learning_rate = 1e-3, n_jobs =-1, random_state = 0, scale_pos_weight=weights, eval_metric='error')
# Instanciando modelo LightGBM
modelo_LightGBM = LGBMClassifier(n_estimators = 1000, max_depth = 8, num_leaves = 2^8, learning_rate = 1e-3, n_jobs =-1, random_state = 0, is_unbalance=True, verbose=-1)
# Instanciando modelo catboost
modelo_CatBoost = CatBoostClassifier(n_estimators = 1000, max_depth = 8, learning_rate = 1e-3, random_state = 0, scale_pos_weight = weights, verbose = 0)
# Instanciando o modelo Balanced Random Forest
modelo_BRandom_Forest = BalancedRandomForestClassifier(n_estimators = 1000, max_depth = 8, random_state = 0, verbose = 0)
# 1) Executar os modelos com a validação cruzada
# 2) fazer uma feature engineering, no próprio código da validação cruzada
# 3) Para o melhor modelo, fazer uma feature selection (RFE que usa feature importance), após o código da validação cruzada
# 4) Por fim, para este melhor modelo, fazer uma tunagem de hiperparâmetros
Construção de uma função de validação cruzada - Stratified K-Fold¶
# Função para aplicação da validação cruzada para obtenção das métricas dos modelos
def validacao_cruzada(X, y, modelo, k, threshold):
# Inicializando a função StratifiedKFold
folds = StratifiedKFold(n_splits=k, shuffle=True, random_state=42)
# Criando listas para armazenar os valores de precisão, revocação, acurácia, medida-F1, precision_recall_auc e roc_auc
# em cada fold
precisoes = list()
revocacoes=list()
acuracias=list()
Medida_F1=list()
precision_recall_auc=list()
rocs_auc=list()
cm_total = np.zeros((2, 2))
# Será aplicado o método "split" no objeto folds, que retornará uma lista
# com os índices das instâncias que pertencem ao conjunto de treino e
# outra com os índices das instâncias que pertencem ao conjunto de teste
for k, (train_index, test_index) in enumerate(folds.split(X,y)):
print("=-"*6 + f"Fold: {k+1}" + "-="*6)
# Dividindo os dados em treino e teste para cada um dos folds
X_train_intern, y_train_intern = X.iloc[train_index, :], y.iloc[train_index]
X_test_intern, y_test_intern = X.iloc[test_index, :], y.iloc[test_index]
# train_index e test_index: São os índices das instâncias do conjunto
# de treino e teste, respectivamente, selecionados em cada um dos folds
###########################################
############## Preprocessing ##############
###########################################
# Instanciando o CatBoost Encoder
encoder = CatBoostEncoder()
# Criando um imputer para preencher com a moda os valores faltantes de variáveis categóricas
cat_imputer = SimpleImputer(strategy='most_frequent')
# Criando um imputer para preencher com a mediana os valores faltantes de variáveis numéricas
num_imputer = SimpleImputer(strategy='median')
# Criando pipelines para variáveis categóricas e numéricas que preenche os valores faltantes
cat_pipeline = Pipeline([('encoder', encoder), ('imputer', cat_imputer)])
num_pipeline = Pipeline([('imputer', num_imputer)])
# feature engineering
# Identifica as variáveis categóricas e numéricas
cat_cols = X_train_intern.select_dtypes(include=['object']).columns
num_cols = X_train_intern.select_dtypes(exclude=['object']).columns
# Aplicando os pipelines no conjunto de treinamento para preencher valores faltantes em colunas categóricas e numéricas
X_train_intern[cat_cols] = cat_pipeline.fit_transform(X_train_intern[cat_cols], y_train_intern)
X_train_intern[num_cols] = num_pipeline.fit_transform(X_train_intern[num_cols])
# Aplicando os pipelines ao conjunto de teste para preencher valores faltantes em colunas categóricas e numéricas
X_test_intern[cat_cols] = cat_pipeline.transform(X_test_intern[cat_cols])
X_test_intern[num_cols] = num_pipeline.transform(X_test_intern[num_cols])
# Treinando o modelo
modelo.fit(X_train_intern, y_train_intern)
# Obtendo as probabilidades de cada registro pertencer a classe 1
y_pred_proba = modelo.predict_proba(X_test_intern)[:, 1]
# Obtendo as previsões do modelo
y_pred = np.where(y_pred_proba > threshold, 1, 0)
# Calculando a precisão e revocação para determinar a precision_recall_auc
precisao, revocacao, limiares = precision_recall_curve(y_test_intern, y_pred)
# Calculando a matriz de confusão do fold
cm_total += confusion_matrix(y_test_intern, y_pred)
# Determinando as métricas para cada fold
precisao_revocacao_auc = auc(revocacao, precisao)
roc_auc = roc_auc_score(y_test_intern, y_pred)
acuracia_score = accuracy_score(y_test_intern, y_pred)
precisao_score = precision_score(y_test_intern, y_pred)
revocacao_score = recall_score(y_test_intern, y_pred)
f1score = f1_score(y_test_intern, y_pred)
# Armazenando as métricas nas listas criadas
precisoes.append(precisao_score)
revocacoes.append(revocacao_score)
precision_recall_auc.append(precisao_revocacao_auc)
rocs_auc.append(roc_auc)
acuracias.append(acuracia_score)
Medida_F1.append(f1score)
# Exibindo as métricas para cada um dos folds
print(f"Precisão: {precisao_score:.4f}")
print(f"Revocação: {revocacao_score:.4f}")
print(f"Acurácia: {acuracia_score:.4f}")
print(f"Medida F1: {f1score:.4f}")
print(f"Precision-Recall AUC: {precisao_revocacao_auc:.4f}")
print(f"ROC AUC: {roc_auc:.4f}")
# Transformando as listas em arrays para fazer operações matemáticas
precisoes = np.array(precisoes)
revocacoes = np.array(revocacoes)
precision_recall_auc = np.array(precision_recall_auc)
rocs_auc = np.array(rocs_auc)
acuracias = np.array(acuracias)
Medida_F1 = np.array(Medida_F1)
# Calculando as médias das métricas
media_revocacao = np.mean(revocacoes)
media_precisao = np.mean(precisoes)
media_acuracia = np.mean(acuracias)
media_F1 = np.mean(Medida_F1)
media_pr_AUC = np.mean(precision_recall_auc)
media_roc_AUC = np.mean(rocs_auc)
# Calculando os desvios padrão para cada métrica
std_revocacao = np.std(revocacoes)
std_precisao = np.std(precisoes)
std_acuracia = np.std(acuracias)
std_F1 = np.std(Medida_F1)
std_pr_AUC = np.std(precision_recall_auc)
std_roc_AUC = np.std(rocs_auc)
# Exibindo as médias das métricas obtidas
print()
print("=-"*6 + "Exibindo a média das métricas obtidas" + "-="*6)
print(f"Média da acurácia: {media_acuracia:.4f} +/- {std_acuracia:.4f}")
print(f"Média da revocação: {media_revocacao:.4f} +/- {std_revocacao:.4f}")
print(f"Média da precisão: {media_precisao:.4f} +/- {std_precisao:.4f}")
print(f"Média da Medida F1: {media_F1:.4f} +/- {std_F1:.4f}")
print(f"Média da ROC AUC: {media_roc_AUC:.4f} +/- {std_roc_AUC:.4f}")
print(f"Média da PR AUC: {media_pr_AUC:.4f} +/- {std_pr_AUC:.4f}")
# Plotando a matriz de confusão agregada com heatmap
plt.figure(figsize=(8, 6))
sns.heatmap(cm_total, annot=True, fmt=".0f", cmap="Blues")
plt.title("Matriz de Confusão Agregada de Todos os Folds")
plt.ylabel('Verdadeiro')
plt.xlabel('Previsto')
plt.show()
Modelo LightGBM¶
validacao_cruzada(X, y, modelo_LightGBM, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.1483 Revocação: 0.9149 Acurácia: 0.8795 Medida F1: 0.2552 Precision-Recall AUC: 0.5326 ROC AUC: 0.8968 =-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.1564 Revocação: 0.9085 Acurácia: 0.8874 Medida F1: 0.2668 Precision-Recall AUC: 0.5335 ROC AUC: 0.8977 =-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.1555 Revocação: 0.9123 Acurácia: 0.8863 Medida F1: 0.2656 Precision-Recall AUC: 0.5348 ROC AUC: 0.8990 =-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.1464 Revocação: 0.9265 Acurácia: 0.8766 Medida F1: 0.2528 Precision-Recall AUC: 0.5372 ROC AUC: 0.9009 =-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.1532 Revocação: 0.9058 Acurácia: 0.8850 Medida F1: 0.2620 Precision-Recall AUC: 0.5306 ROC AUC: 0.8952 =-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-= Média da acurácia: 0.8830 +/- 0.0042 Média da revocação: 0.9136 +/- 0.0071 Média da precisão: 0.1519 +/- 0.0040 Média da Medida F1: 0.2605 +/- 0.0056 Média da ROC AUC: 0.8979 +/- 0.0020 Média da PR AUC: 0.5337 +/- 0.0022
Modelo XGBoost¶
validacao_cruzada(X, y, modelo_XGBoost, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.1417 Revocação: 0.9343 Acurácia: 0.8708 Medida F1: 0.2461 Precision-Recall AUC: 0.5387 ROC AUC: 0.9018 =-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.1067 Revocação: 0.9729 Acurácia: 0.8155 Medida F1: 0.1923 Precision-Recall AUC: 0.5401 ROC AUC: 0.8924 =-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.1018 Revocação: 0.9652 Acurácia: 0.8072 Medida F1: 0.1841 Precision-Recall AUC: 0.5339 ROC AUC: 0.8844 =-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.1093 Revocação: 0.9665 Acurácia: 0.8217 Medida F1: 0.1963 Precision-Recall AUC: 0.5382 ROC AUC: 0.8924 =-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.1181 Revocação: 0.9548 Acurácia: 0.8384 Medida F1: 0.2103 Precision-Recall AUC: 0.5370 ROC AUC: 0.8953 =-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-= Média da acurácia: 0.8307 +/- 0.0225 Média da revocação: 0.9587 +/- 0.0135 Média da precisão: 0.1155 +/- 0.0141 Média da Medida F1: 0.2058 +/- 0.0219 Média da ROC AUC: 0.8933 +/- 0.0056 Média da PR AUC: 0.5376 +/- 0.0021
Modelo CatBoost¶
validacao_cruzada(X, y, modelo_CatBoost, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.0841 Revocação: 1.0000 Acurácia: 0.7542 Medida F1: 0.1551 Precision-Recall AUC: 0.5420 ROC AUC: 0.8742 =-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.0835 Revocação: 0.9974 Acurácia: 0.7528 Medida F1: 0.1540 Precision-Recall AUC: 0.5405 ROC AUC: 0.8723 =-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.0833 Revocação: 1.0000 Acurácia: 0.7520 Medida F1: 0.1538 Precision-Recall AUC: 0.5417 ROC AUC: 0.8732 =-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.0827 Revocação: 0.9987 Acurácia: 0.7505 Medida F1: 0.1528 Precision-Recall AUC: 0.5407 ROC AUC: 0.8717 =-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.0850 Revocação: 0.9948 Acurácia: 0.7585 Medida F1: 0.1566 Precision-Recall AUC: 0.5400 ROC AUC: 0.8740 =-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-= Média da acurácia: 0.7536 +/- 0.0027 Média da revocação: 0.9982 +/- 0.0019 Média da precisão: 0.0837 +/- 0.0008 Média da Medida F1: 0.1545 +/- 0.0013 Média da ROC AUC: 0.8731 +/- 0.0010 Média da PR AUC: 0.5410 +/- 0.0008
Modelo Balanced Random Forest¶
validacao_cruzada(X, y, modelo_BRandom_Forest, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.0885 Revocação: 0.9910 Acurácia: 0.7694 Medida F1: 0.1624 Precision-Recall AUC: 0.5398 ROC AUC: 0.8776 =-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.0840 Revocação: 0.9961 Acurácia: 0.7547 Medida F1: 0.1549 Precision-Recall AUC: 0.5401 ROC AUC: 0.8726 =-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.0843 Revocação: 0.9987 Acurácia: 0.7554 Medida F1: 0.1554 Precision-Recall AUC: 0.5415 ROC AUC: 0.8742 =-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.0829 Revocação: 0.9987 Acurácia: 0.7511 Medida F1: 0.1531 Precision-Recall AUC: 0.5408 ROC AUC: 0.8721 =-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.0872 Revocação: 0.9897 Acurácia: 0.7662 Medida F1: 0.1602 Precision-Recall AUC: 0.5385 ROC AUC: 0.8754 =-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-= Média da acurácia: 0.7594 +/- 0.0071 Média da revocação: 0.9948 +/- 0.0038 Média da precisão: 0.0854 +/- 0.0021 Média da Medida F1: 0.1572 +/- 0.0035 Média da ROC AUC: 0.8744 +/- 0.0020 Média da PR AUC: 0.5402 +/- 0.0010
Entre os modelos escolhidos para avaliar a métrica revocação, o XGBoost foi o que melhor performou, com:
- uma revocação média de 0.9587, ou seja, de todos os casos que realmente eram fraude, ele identificou 95,87%.
- uma ROC AUC média de 0.8933, ou seja, há 89,33% de que o modelo classifique corretamente um pedido aleatório de fraude como fraude e um pedido aleatório de não fraude como não fraude.
- uma Medida F1 média de 0.2058, o que indica que o modelo ainda possui muitos falsos positivos (precisão baixa).
Feature Selection¶
# Inicializando o RFE
rfe = RFE(estimator = modelo_XGBoost, n_features_to_select = 21, step = 1)
encoder = CatBoostEncoder()
# Ajustar e transformar os dados de treinamento
X_train_encoded = encoder.fit_transform(X_train, y_train)
# Transformar os dados de teste
X_test_encoded = encoder.transform(X_test)
# Treinando o RFE
rfe.fit(X_train_encoded, y_train)
# Obtendo as features selecionadas
features_selecionadas = np.array(list(X_train_encoded.columns))[rfe.support_]
features_selecionadas
array(['Type', 'Benefit per order', 'Sales per customer', 'Category Id',
'Customer City', 'Customer Country', 'Customer Segment',
'Customer State', 'Market', 'Order City', 'Order Country',
'Order Item Discount', 'Order Item Discount Rate',
'Order Item Product Price', 'Order Item Profit Ratio',
'Order Item Quantity', 'Sales', 'Order Region', 'Order State',
'Product Name', 'Shipping Mode'], dtype='<U24')
validacao_cruzada(X[features_selecionadas], y, modelo_XGBoost, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.1417 Revocação: 0.9343 Acurácia: 0.8708 Medida F1: 0.2461 Precision-Recall AUC: 0.5387 ROC AUC: 0.9018 =-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.1067 Revocação: 0.9729 Acurácia: 0.8155 Medida F1: 0.1923 Precision-Recall AUC: 0.5401 ROC AUC: 0.8924 =-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.1018 Revocação: 0.9652 Acurácia: 0.8072 Medida F1: 0.1841 Precision-Recall AUC: 0.5339 ROC AUC: 0.8844 =-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.1093 Revocação: 0.9665 Acurácia: 0.8217 Medida F1: 0.1963 Precision-Recall AUC: 0.5382 ROC AUC: 0.8924 =-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.1181 Revocação: 0.9548 Acurácia: 0.8384 Medida F1: 0.2103 Precision-Recall AUC: 0.5370 ROC AUC: 0.8953 =-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-= Média da acurácia: 0.8307 +/- 0.0225 Média da revocação: 0.9587 +/- 0.0135 Média da precisão: 0.1155 +/- 0.0141 Média da Medida F1: 0.2058 +/- 0.0219 Média da ROC AUC: 0.8933 +/- 0.0056 Média da PR AUC: 0.5376 +/- 0.0021
Neste caso, realizando uma feature selection e excluindo as 3 colunas menos importantes para o modelos, atingimos o mesmo resultado que com o dataset original. Assim, por questões de processamento, é preferível manter o dataset com as 3 colunas a menos.
Tunagem de Hiperparâmetros para o XGBoost¶
def tunagem_hiperparametros(trial, k = 5, threshold = 0.5):
# Parâmetros para serem tunados
learning_rate = trial.suggest_float('learning_rate', 1e-3, 1e-1, log=True)
max_depth = trial.suggest_int('max_depth', 1, 20)
subsample = trial.suggest_float('subsample', 0.5, 1, step = 0.1)
colsample_bytree = trial.suggest_float('colsample_bytree', 0.5, 1, step = 0.1)
min_child_weight = trial.suggest_int('min_child_weight', 1, 20)
# Inicializando a função StratifiedKFold
folds = StratifiedKFold(n_splits=k, shuffle=True, random_state=42)
# Criando listas para armazenar os valores de precisão, revocação, acurácia, medida-F1, precision_recall_auc e roc_auc
# em cada fold
precisoes = list()
revocacoes=list()
acuracias=list()
Medida_F1=list()
precision_recall_auc=list()
rocs_auc=list()
# Será aplicado o método "split" no objeto folds, que retornará uma lista
# com os índices das instâncias que pertencem ao conjunto de treino e
# outra com os índices das instâncias que pertencem ao conjunto de teste
for k, (train_index, test_index) in enumerate(folds.split(X[features_selecionadas],y)):
print("=-"*6 + f"Fold: {k+1}" + "-="*6)
# Dividindo os dados em treino e teste para cada um dos folds
X_train_intern, y_train_intern = X[features_selecionadas].iloc[train_index, :], y.iloc[train_index]
X_test_intern, y_test_intern = X[features_selecionadas].iloc[test_index, :], y.iloc[test_index]
# train_index e test_index: São os índices das instâncias do conjunto
# de treino e teste, respectivamente, selecionados em cada um dos folds
###########################################
############## Preprocessing ##############
###########################################
# Instanciando o CatBoost Encoder
encoder = CatBoostEncoder()
# Criando um imputer para preencher com a moda os valores faltantes de variáveis categóricas
cat_imputer = SimpleImputer(strategy='most_frequent')
# Criando um imputer para preencher com a mediana os valores faltantes de variáveis numéricas
num_imputer = SimpleImputer(strategy='median')
# Criando pipelines para variáveis categóricas e numéricas que preenche os valores faltantes
cat_pipeline = Pipeline([('encoder', encoder), ('imputer', cat_imputer)])
num_pipeline = Pipeline([('imputer', num_imputer)])
# Identifica as variáveis categóricas e numéricas
cat_cols = X_train_intern.select_dtypes(include=['object']).columns
num_cols = X_train_intern.select_dtypes(exclude=['object']).columns
# Aplicando os pipelines no conjunto de treinamento para preencher valores faltantes em colunas categóricas e numéricas
X_train_intern[cat_cols] = cat_pipeline.fit_transform(X_train_intern[cat_cols], y_train_intern)
X_train_intern[num_cols] = num_pipeline.fit_transform(X_train_intern[num_cols])
# Aplicando os pipelines ao conjunto de teste para preencher valores faltantes em colunas categóricas e numéricas
X_test_intern[cat_cols] = cat_pipeline.transform(X_test_intern[cat_cols])
X_test_intern[num_cols] = num_pipeline.transform(X_test_intern[num_cols])
# Instanciando o Modelo XGBoost
modelo_XGBoost = XGBClassifier(n_estimators = 1000, max_depth = max_depth, learning_rate = learning_rate,
subsample = subsample, colsample_bytree = colsample_bytree, min_child_weight = min_child_weight,
n_jobs =-1, random_state = 0, scale_pos_weight=weights, eval_metric='error')
# Treinando o modelo XGBoost
modelo_XGBoost.fit(X_train_intern, y_train_intern)
# Obtendo as probabilidades de cada registro pertencer a classe 1
y_pred_proba = modelo_XGBoost.predict_proba(X_test_intern)[:, 1]
# Obtendo as previsões do modelo
y_pred = np.where(y_pred_proba > threshold, 1, 0)
# Calculando a precisão e revocação para determinar a precision_recall_auc
precisao, revocacao, limiares = precision_recall_curve(y_test_intern, y_pred)
# Determinando as métricas para cada fold
precisao_revocacao_auc = auc(revocacao, precisao)
roc_auc = roc_auc_score(y_test_intern, y_pred)
acuracia_score = accuracy_score(y_test_intern, y_pred)
precisao_score = precision_score(y_test_intern, y_pred)
revocacao_score = recall_score(y_test_intern, y_pred)
f1score = f1_score(y_test_intern, y_pred)
# Armazenando as métricas nas listas criadas
precisoes.append(precisao_score)
revocacoes.append(revocacao_score)
precision_recall_auc.append(precisao_revocacao_auc)
rocs_auc.append(roc_auc)
acuracias.append(acuracia_score)
Medida_F1.append(f1score)
# Transformando as listas em arrays para fazer operações matemáticas
precisoes = np.array(precisoes)
revocacoes = np.array(revocacoes)
precision_recall_auc = np.array(precision_recall_auc)
rocs_auc = np.array(rocs_auc)
acuracias = np.array(acuracias)
Medida_F1 = np.array(Medida_F1)
# Calculando as médias das métricas
media_revocacao = np.mean(revocacoes)
media_precisao = np.mean(precisoes)
media_acuracia = np.mean(acuracias)
media_F1 = np.mean(Medida_F1)
media_pr_AUC = np.mean(precision_recall_auc)
media_roc_AUC = np.mean(rocs_auc)
# Calculando os desvios padrão para cada métrica
std_revocacao = np.std(revocacoes)
std_precisao = np.std(precisoes)
std_acuracia = np.std(acuracias)
std_F1 = np.std(Medida_F1)
std_pr_AUC = np.std(precision_recall_auc)
std_roc_AUC = np.std(rocs_auc)
return media_roc_AUC
study = opt.create_study(direction='maximize')
study.optimize(tunagem_hiperparametros, n_trials = 20)
[I 2024-02-10 22:50:27,117] A new study created in memory with name: no-name-75910072-fd13-43c0-9c8c-4c56e7363ec8
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:51:34,699] Trial 0 finished with value: 0.8795904611780354 and parameters: {'learning_rate': 0.07994256492342769, 'max_depth': 1, 'subsample': 0.5, 'colsample_bytree': 0.8, 'min_child_weight': 3}. Best is trial 0 with value: 0.8795904611780354.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:53:38,275] Trial 1 finished with value: 0.8195058699436866 and parameters: {'learning_rate': 0.03857576834279582, 'max_depth': 7, 'subsample': 0.9, 'colsample_bytree': 0.5, 'min_child_weight': 6}. Best is trial 0 with value: 0.8795904611780354.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:54:54,145] Trial 2 finished with value: 0.8748614167956793 and parameters: {'learning_rate': 0.010246492195636328, 'max_depth': 2, 'subsample': 0.6, 'colsample_bytree': 0.8, 'min_child_weight': 15}. Best is trial 0 with value: 0.8795904611780354.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 22:58:49,145] Trial 3 finished with value: 0.8842436038946081 and parameters: {'learning_rate': 0.001155522474633838, 'max_depth': 12, 'subsample': 1.0, 'colsample_bytree': 0.6, 'min_child_weight': 3}. Best is trial 3 with value: 0.8842436038946081.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:00:23,475] Trial 4 finished with value: 0.8874130681818576 and parameters: {'learning_rate': 0.011912842014604687, 'max_depth': 3, 'subsample': 0.6, 'colsample_bytree': 0.7, 'min_child_weight': 12}. Best is trial 4 with value: 0.8874130681818576.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:02:31,563] Trial 5 finished with value: 0.7679785058549813 and parameters: {'learning_rate': 0.09516642910100707, 'max_depth': 6, 'subsample': 0.8, 'colsample_bytree': 1.0, 'min_child_weight': 19}. Best is trial 4 with value: 0.8874130681818576.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:05:51,242] Trial 6 finished with value: 0.7251617956877033 and parameters: {'learning_rate': 0.032019024764589774, 'max_depth': 11, 'subsample': 0.6, 'colsample_bytree': 1.0, 'min_child_weight': 14}. Best is trial 4 with value: 0.8874130681818576.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:11:36,789] Trial 7 finished with value: 0.740677298749992 and parameters: {'learning_rate': 0.009486734213922966, 'max_depth': 19, 'subsample': 0.6, 'colsample_bytree': 1.0, 'min_child_weight': 9}. Best is trial 4 with value: 0.8874130681818576.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:15:25,768] Trial 8 finished with value: 0.6253362874109603 and parameters: {'learning_rate': 0.07718828159729633, 'max_depth': 16, 'subsample': 0.9, 'colsample_bytree': 0.6, 'min_child_weight': 8}. Best is trial 4 with value: 0.8874130681818576.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:16:55,594] Trial 9 finished with value: 0.8977030594763274 and parameters: {'learning_rate': 0.06716806389913002, 'max_depth': 3, 'subsample': 0.9, 'colsample_bytree': 1.0, 'min_child_weight': 1}. Best is trial 9 with value: 0.8977030594763274.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:18:43,054] Trial 10 finished with value: 0.8976164627035237 and parameters: {'learning_rate': 0.002689437912090455, 'max_depth': 7, 'subsample': 1.0, 'colsample_bytree': 0.9, 'min_child_weight': 1}. Best is trial 9 with value: 0.8977030594763274.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:20:33,669] Trial 11 finished with value: 0.8952878930728498 and parameters: {'learning_rate': 0.001699179220927879, 'max_depth': 7, 'subsample': 1.0, 'colsample_bytree': 0.9, 'min_child_weight': 1}. Best is trial 9 with value: 0.8977030594763274.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:22:16,183] Trial 12 finished with value: 0.8881887720731031 and parameters: {'learning_rate': 0.0027558417106266398, 'max_depth': 5, 'subsample': 0.9, 'colsample_bytree': 0.9, 'min_child_weight': 1}. Best is trial 9 with value: 0.8977030594763274.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:25:00,732] Trial 13 finished with value: 0.9011311412130854 and parameters: {'learning_rate': 0.004259837949378204, 'max_depth': 9, 'subsample': 0.8, 'colsample_bytree': 0.9, 'min_child_weight': 5}. Best is trial 13 with value: 0.9011311412130854.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:29:34,596] Trial 14 finished with value: 0.7954917222923114 and parameters: {'learning_rate': 0.005175092414261333, 'max_depth': 14, 'subsample': 0.8, 'colsample_bytree': 0.9, 'min_child_weight': 6}. Best is trial 13 with value: 0.9011311412130854.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:32:26,844] Trial 15 finished with value: 0.7971755825236846 and parameters: {'learning_rate': 0.020161930390439852, 'max_depth': 9, 'subsample': 0.7, 'colsample_bytree': 1.0, 'min_child_weight': 5}. Best is trial 13 with value: 0.9011311412130854.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:34:56,655] Trial 16 finished with value: 0.8995262369826722 and parameters: {'learning_rate': 0.005083639900788686, 'max_depth': 9, 'subsample': 0.8, 'colsample_bytree': 0.8, 'min_child_weight': 4}. Best is trial 13 with value: 0.9011311412130854.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-10 23:37:38,630] Trial 17 finished with value: 0.891385851763976 and parameters: {'learning_rate': 0.00518511506281332, 'max_depth': 10, 'subsample': 0.7, 'colsample_bytree': 0.7, 'min_child_weight': 9}. Best is trial 13 with value: 0.9011311412130854.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-11 11:08:17,560] Trial 18 finished with value: 0.7776167122971407 and parameters: {'learning_rate': 0.004786985768187549, 'max_depth': 14, 'subsample': 0.8, 'colsample_bytree': 0.8, 'min_child_weight': 4}. Best is trial 13 with value: 0.9011311412130854.
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
=-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
[I 2024-02-11 11:11:21,440] Trial 19 finished with value: 0.8504330954716037 and parameters: {'learning_rate': 0.003381270572925951, 'max_depth': 13, 'subsample': 0.7, 'colsample_bytree': 0.7, 'min_child_weight': 7}. Best is trial 13 with value: 0.9011311412130854.
params = {'learning_rate': 0.006273985142858805, 'max_depth': 7, 'subsample': 0.7, 'colsample_bytree': 0.8, 'min_child_weight': 12}
# XGBoost executado para os melhores parâmetros
modelo_XGBoost = XGBClassifier(n_estimators = 1000, n_jobs =-1, random_state = 0, scale_pos_weight=weights, eval_metric='error', **params)
validacao_cruzada(X[features_selecionadas], y, modelo_XGBoost, k = 5, threshold = 0.5)
=-=-=-=-=-=-Fold: 1-=-=-=-=-=-=
Precisão: 0.1431 Revocação: 0.9369 Acurácia: 0.8720 Medida F1: 0.2482 Precision-Recall AUC: 0.5407 ROC AUC: 0.9037 =-=-=-=-=-=-Fold: 2-=-=-=-=-=-=
Precisão: 0.1261 Revocação: 0.9665 Acurácia: 0.8481 Medida F1: 0.2230 Precision-Recall AUC: 0.5467 ROC AUC: 0.9059 =-=-=-=-=-=-Fold: 3-=-=-=-=-=-=
Precisão: 0.1359 Revocação: 0.9484 Acurácia: 0.8629 Medida F1: 0.2377 Precision-Recall AUC: 0.5427 ROC AUC: 0.9047 =-=-=-=-=-=-Fold: 4-=-=-=-=-=-=
Precisão: 0.1282 Revocação: 0.9535 Acurácia: 0.8528 Medida F1: 0.2260 Precision-Recall AUC: 0.5414 ROC AUC: 0.9020 =-=-=-=-=-=-Fold: 5-=-=-=-=-=-=
Precisão: 0.1356 Revocação: 0.9484 Acurácia: 0.8626 Medida F1: 0.2373 Precision-Recall AUC: 0.5426 ROC AUC: 0.9045 =-=-=-=-=-=-Exibindo a média das métricas obtidas-=-=-=-=-=-= Média da acurácia: 0.8597 +/- 0.0084 Média da revocação: 0.9507 +/- 0.0096 Média da precisão: 0.1338 +/- 0.0061 Média da Medida F1: 0.2344 +/- 0.0091 Média da ROC AUC: 0.9042 +/- 0.0013 Média da PR AUC: 0.5428 +/- 0.0021
Com a realização da tunagem de hiperparâmetros:
- A ROC AUC saltou de 0.8933 para 0.9042
- A revocação praticamente se manteve a mesma
- A precisão saltou de 0.1155 para 0.1338
- A Medida F1 saltou de 0.2058 para 0.2344
- A PR AUC saltou de 0.5376 para 0.5428
- A acurácia saltou de 0.8307 para 0.8597
No geral houve uma pequena melhora no modelo.
O quanto a DataCo Global ganhou ao identificar corretamente as fraudes?¶
encoder = CatBoostEncoder()
X_train_encoded = encoder.fit_transform(X_train[features_selecionadas], y_train)
X_test_encoded = encoder.transform(X_test[features_selecionadas])
# Fazendo previsões de probabilidade de fraude para o conjunto de teste
xgb_probs = modelo_XGBoost.predict_proba(X_test_encoded)[:, 1]
# Aqui são trazidas as probabilidades encontradas pelo XGBoost
df_test = X_test[features_selecionadas].copy()
df_test['fraude'] = y_test
df_test['XGB_Prob'] = xgb_probs
# Função para calcular o impacto financeiro das decisões de bloqueio de transações
def calculo_impacto_financeiro(df, blocked_col, fraud_col, profit_col):
# Calculando perdas por fraude (transações que são fraudes e não foram bloqueadas)
df['fraud_loss'] = ((df[fraud_col]) & (~df[blocked_col])) * df[profit_col]
# lucro obtido de transações legítimas que o sistema corretamente identificou como não fraudulentas e, portanto, não bloqueou.
df['saved_profit'] = ((~df[fraud_col]) & (~df[blocked_col])) * df[profit_col]
# Representa o lucro líquido após considerar tanto as perdas por fraude quanto o lucro preservado.
df['total_profit'] = df['saved_profit'] - df['fraud_loss']
return df[['fraud_loss', 'saved_profit', 'total_profit']].sum()
# Definindo uma gama de limiares possíveis
possiveis_thresholds = np.linspace(0.01, 0.99, 99)
# Inicializando uma lista para armazenar os resultados
impactos_financeiros = []
# Testando cada limiar
for threshold in possiveis_thresholds:
# Aplicando o limiar atual
df_test['blocked'] = df_test['XGB_Prob'] >= threshold
# Calculando o impacto financeiro para o limiar atual
impacto = calculo_impacto_financeiro(df_test, 'blocked', 'fraude', 'Benefit per order')
# Armazenando os resultados, incluindo o limiar
impactos_financeiros.append({
'threshold': threshold,
'Perda por fraude': impacto['fraud_loss'],
'Lucro Salvo': impacto['saved_profit'],
'Lucro Total': impacto['total_profit']
})
# Convertendo os resultados em um DataFrame
results_df = pd.DataFrame(impactos_financeiros)
# Encontrando o limiar com o maior lucro
best_result = results_df.loc[results_df['Lucro Total'].idxmax()]
best_result_df = pd.DataFrame([best_result])
# Exibindo o melhor limiar e o lucro associado
best_result_df
| threshold | Perda por fraude | Lucro Salvo | Lucro Total | |
|---|---|---|---|---|
| 96 | 0.97 | 24241.450006 | 1.090407e+06 | 1.066166e+06 |
print(f"O modelo conseguiu lucrar {best_result_df['Lucro Salvo'].iloc[0]:.2f} ao identificar corretamente operações fraudulentas!")
print(f"Porém, modelo perdeu {best_result_df['Perda por fraude'].iloc[0]:.2f} por não identificar corretamente outras operações que realmente eram fraudes!")
print(f"Considerando as perdas por fraude, o modelo conseguiu lucrar {best_result_df['Lucro Total'].iloc[0]:.2f} ao total!")
O modelo conseguiu lucrar 1090407.07 ao identificar corretamente operações fraudulentas! Porém, modelo perdeu 24241.45 por não identificar corretamente outras operações que realmente eram fraudes! Considerando as perdas por fraude, o modelo conseguiu lucrar 1066165.62 ao total!